Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0bde3b3e6 | ||
|
|
73c5b3383e | ||
|
|
5f09fb3bde | ||
|
|
0610fd6fbb | ||
|
|
85c685a5ca | ||
|
|
2977506cb8 | ||
|
|
538d734a5b | ||
|
|
e680f429eb | ||
|
|
c308850eb3 | ||
|
|
8e941233a0 | ||
|
|
22a8a206ab | ||
|
|
773249ca4f | ||
|
|
599d02ea0e | ||
|
|
675316c9b7 | ||
|
|
c22fd8b132 | ||
|
|
c4b008f910 | ||
|
|
0cde5a3607 | ||
|
|
6180b9f86c | ||
|
|
daae7d8523 | ||
|
|
540e5b5ac0 | ||
|
|
4b671001e0 | ||
|
|
1ae4bed339 | ||
|
|
04e734bd5a | ||
|
|
a0e71ef444 | ||
|
|
05d8da36d8 | ||
|
|
5cae61b896 | ||
|
|
a319e84831 | ||
|
|
d38781400e | ||
|
|
cbcc6647f2 | ||
|
|
d2a217319d | ||
|
|
280b564ab3 | ||
|
|
0e14b5bd04 | ||
|
|
90ded3e1cb | ||
|
|
e795b3369d | ||
|
|
2f26c7db96 | ||
|
|
ed588ba43c | ||
|
|
c9f63138a0 | ||
|
|
97f779dc83 | ||
|
|
2753446a57 | ||
|
|
163eef58c2 | ||
|
|
201bf75bf7 | ||
|
|
dd4d0d4607 | ||
|
|
88bbaea4c9 | ||
|
|
ac37ebb4e2 | ||
|
|
16c3abf3f2 | ||
|
|
d1681c3d88 | ||
|
|
97099e9f65 | ||
|
|
4e8cb19f55 | ||
|
|
f7280c1a90 | ||
|
|
bfd6c5d2bd | ||
|
|
47eab96a67 | ||
|
|
ec7cba8f49 | ||
|
|
e0d497796a | ||
|
|
c5bc02d45d | ||
|
|
e2b713282c | ||
|
|
05c883fa3e | ||
|
|
e5d9680b23 | ||
|
|
4a40483348 | ||
|
|
5f12d1ce0e | ||
|
|
050b2303f0 | ||
|
|
a579f5736b | ||
|
|
82294983b0 | ||
|
|
8558ea29df | ||
|
|
9211074b32 | ||
|
|
2da4371847 | ||
|
|
791bf9fc8d | ||
|
|
e4b324d209 | ||
|
|
3b0bd172ba | ||
|
|
91e1724540 | ||
|
|
fcdf3c9240 | ||
|
|
92d92b8021 | ||
|
|
dc5bd2e71e | ||
|
|
36fc0f0e10 | ||
|
|
52a781dee8 | ||
|
|
15d60a3a7b | ||
|
|
0a1b9ce27a | ||
|
|
bb9429e907 | ||
|
|
1213854644 | ||
|
|
e3d4100055 | ||
|
|
418426419d | ||
|
|
707048a1df | ||
|
|
e9ab1fd16e | ||
|
|
cc74359e83 | ||
|
|
9be862e37b | ||
|
|
2133c1e16a | ||
|
|
e46d4cff27 | ||
|
|
38b362e5fe | ||
|
|
6d01461e53 | ||
|
|
942dd89ea3 | ||
|
|
d40d4ec7fb | ||
|
|
96f720c195 | ||
|
|
3f5c54d5de | ||
|
|
c62805ae57 | ||
|
|
acb1b72036 | ||
|
|
443f452689 | ||
|
|
72a347aa9f | ||
|
|
ab7ffa0d94 | ||
|
|
1259439603 | ||
|
|
208ed63c96 | ||
|
|
dd439d5ec3 | ||
|
|
44a14f326b | ||
|
|
4d4a9523f8 | ||
|
|
b24a676f22 | ||
|
|
4e692d608c | ||
|
|
b54eb0cca9 | ||
|
|
24ad2f45cc | ||
|
|
27b5ce8d60 | ||
|
|
1ce350b12e | ||
|
|
405d53220b | ||
|
|
3123e90d5a | ||
|
|
ea7a9811e4 | ||
|
|
74b04ff922 | ||
|
|
4b24cd68ce | ||
|
|
c94089842a | ||
|
|
9c0743d668 | ||
|
|
d023af20a0 | ||
|
|
b81ddd8dd9 | ||
|
|
c1e7f58567 | ||
|
|
8c055d0b72 | ||
|
|
44a30f7a0d | ||
|
|
f837ad2270 | ||
|
|
6b909bd0e3 | ||
|
|
23d7dea0bb | ||
|
|
fc13053545 | ||
|
|
e874ccb5b1 | ||
|
|
fd11e8c1fe | ||
|
|
294c5ab06d | ||
|
|
576dac3aff | ||
|
|
4b80310c0d | ||
|
|
bd9cb2e257 | ||
|
|
64733d55f8 | ||
|
|
88483af607 | ||
|
|
1726e023ea | ||
|
|
7e9b12f818 | ||
|
|
834a9c9ac5 | ||
|
|
3f098cd33a | ||
|
|
4193d61e0b | ||
|
|
e8d464a244 | ||
|
|
4c1c375eef | ||
|
|
65a2bbae0a | ||
|
|
57616aba62 | ||
|
|
4396bd4625 | ||
|
|
df4f5685be | ||
|
|
1e5f797195 | ||
|
|
8f06ceaedd | ||
|
|
6b58b72028 | ||
|
|
51dc1b1618 | ||
|
|
be5549bf81 | ||
|
|
c4b7bd4d71 | ||
|
|
f25eb2901e | ||
|
|
65025b57c5 | ||
|
|
8e3b212df0 | ||
|
|
d93af3d6e4 | ||
|
|
dd0c17a9d4 | ||
|
|
d334726991 | ||
|
|
9aea4e5677 | ||
|
|
ffd9efeb98 | ||
|
|
5bf3bd7ce5 | ||
|
|
693fce1bb1 | ||
|
|
48fbb1076f | ||
|
|
7bc994b99f | ||
|
|
8586d1cc12 | ||
|
|
de280c23ea | ||
|
|
06887003cf | ||
|
|
e2eacbc051 | ||
|
|
93b81493c0 | ||
|
|
4287f60ab3 | ||
|
|
f62e9baa4a | ||
|
|
2d2da6e939 | ||
|
|
2bf3d5372b | ||
|
|
a4f8e20293 | ||
|
|
ac915e8973 | ||
|
|
e19a521c4e | ||
|
|
bafea756ee | ||
|
|
1bdfc8a5d2 | ||
|
|
6e6285c5ae | ||
|
|
f49411b3a9 | ||
|
|
1d3dee1950 | ||
|
|
caf3ddd68a | ||
|
|
17b5168bde | ||
|
|
9bfc16a4d5 | ||
|
|
98f5245317 | ||
|
|
4286c528f5 | ||
|
|
8f5f9d6238 | ||
|
|
26d3a53d44 | ||
|
|
de8e444aba | ||
|
|
35da556697 | ||
|
|
52ac5e5424 | ||
|
|
517206364a | ||
|
|
5e82eea9f1 | ||
|
|
b6e8768767 | ||
|
|
5df3d4f63b | ||
|
|
c59e9fc3f5 | ||
|
|
febe9871b9 | ||
|
|
4d5d881af6 | ||
|
|
2735deff73 | ||
|
|
ee93add6fc | ||
|
|
8ffab2ae70 | ||
|
|
fa216dddd5 | ||
|
|
34ec39a858 | ||
|
|
7dd611b61a | ||
|
|
5d017258b7 | ||
|
|
e58c00ecdf | ||
|
|
b88848ba8a | ||
|
|
6b6e7c647a | ||
|
|
b972422cc6 | ||
|
|
52ded23315 | ||
|
|
b3399f419e | ||
|
|
e2d60dde43 | ||
|
|
214b3c75d0 | ||
|
|
95659839b2 | ||
|
|
cab3f57737 | ||
|
|
dbbd01a83d | ||
|
|
90e66841df | ||
|
|
5ba41c6ea0 | ||
|
|
63c0de42db | ||
|
|
5646a22245 | ||
|
|
0e13801a7a | ||
|
|
7b0f7d639a | ||
|
|
301766f4aa | ||
|
|
6bdbfa2657 | ||
|
|
f5dafe6d43 | ||
|
|
623f25e521 | ||
|
|
a0e8a7f935 | ||
|
|
61bd31e421 | ||
|
|
84425e1319 | ||
|
|
09615f8ec4 | ||
|
|
3d368482bb | ||
|
|
df2cec5a94 | ||
|
|
369512c6c1 | ||
|
|
10a4cc270a | ||
|
|
1eda5e5860 | ||
|
|
3fc0d9418f | ||
|
|
c4f243f4b0 | ||
|
|
e057d740d8 | ||
|
|
a20c5677b4 | ||
|
|
7f687051f8 | ||
|
|
450c5c9829 | ||
|
|
0ab019e5ab | ||
|
|
40b3fa91ac | ||
|
|
f59c8da000 | ||
|
|
3555d01b68 | ||
|
|
b201b32b46 | ||
|
|
89f79e0977 | ||
|
|
aa0233f49b | ||
|
|
df4a45afb2 | ||
|
|
def0144dab | ||
|
|
ce16fa154c | ||
|
|
063a21997b | ||
|
|
92fce802c5 | ||
|
|
f76d597cbf | ||
|
|
ecbbbc2d6b | ||
|
|
2bb00a177e | ||
|
|
e13b2d4edc | ||
|
|
bb3a94eb84 | ||
|
|
49b460eee3 | ||
|
|
28da1a0ec9 | ||
|
|
6176e6ff40 | ||
|
|
4406287a5f | ||
|
|
3f63f289ed | ||
|
|
5c07dd3023 | ||
|
|
fe71591200 | ||
|
|
3f87e23bd7 | ||
|
|
9d2d99b5ba | ||
|
|
76df43fe66 | ||
|
|
0d72f6b987 | ||
|
|
507b6b9133 | ||
|
|
4911d61d78 | ||
|
|
bdf5f82959 | ||
|
|
86310083f2 | ||
|
|
29faa4932d | ||
|
|
f498bdabb3 | ||
|
|
f4a1f06ba6 | ||
|
|
317b0fc4d7 | ||
|
|
5d081f27d1 | ||
|
|
9ca126f3cd | ||
|
|
736d447d9e | ||
|
|
f2f27ac639 | ||
|
|
90f6fd83ba | ||
|
|
d9e9465415 | ||
|
|
f73fc49ee4 | ||
|
|
afe448647c | ||
|
|
091242bedc | ||
|
|
8d4f76156b | ||
|
|
50eacba438 | ||
|
|
5273dfbbf3 | ||
|
|
db5632d3ba | ||
|
|
9898a8424a | ||
|
|
0f354cfe9b | ||
|
|
b433736241 | ||
|
|
cee48c1f45 | ||
|
|
6d1ea96e79 | ||
|
|
5053f3f0f8 | ||
|
|
7b16c4ebe1 | ||
|
|
b8636029c9 | ||
|
|
357158db3e | ||
|
|
796ee5516f |
@@ -1,8 +1,9 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
.tmp
|
||||
.vagrant
|
||||
results/*
|
||||
tmp
|
||||
coverage
|
||||
front/build
|
||||
|
||||
har.json
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
har.json
|
||||
.idea/
|
||||
@@ -1,18 +0,0 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- "6.2"
|
||||
- "4.0"
|
||||
env:
|
||||
- CXX=g++-4.8
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
before_install:
|
||||
- "npm install -g npm"
|
||||
- "npm install -g grunt-cli"
|
||||
install:
|
||||
- "npm install"
|
||||
@@ -0,0 +1 @@
|
||||
ko_fi: gaelmetais
|
||||
@@ -1,356 +0,0 @@
|
||||
module.exports = function(grunt) {
|
||||
|
||||
// Load all grunt modules
|
||||
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
|
||||
|
||||
// Tell our Express server that Grunt launched it
|
||||
process.env.GRUNTED = true;
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
settings: grunt.file.readJSON('./server_config/settings.json'),
|
||||
|
||||
webfont: {
|
||||
icons: {
|
||||
src: 'front/src/fonts/svg-icons/*.svg',
|
||||
dest: 'tmp',
|
||||
destCss: 'front/src/less',
|
||||
options: {
|
||||
engine: 'node',
|
||||
types: 'woff',
|
||||
stylesheet: 'less',
|
||||
embed: true,
|
||||
htmlDemo: false,
|
||||
syntax: 'bootstrap'
|
||||
}
|
||||
}
|
||||
},
|
||||
less: {
|
||||
all: {
|
||||
files: [
|
||||
{
|
||||
expand: true,
|
||||
cwd: 'front/src/less/',
|
||||
src: ['**/*.less'],
|
||||
dest: 'front/src/css/',
|
||||
ext: '.css'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
replace: {
|
||||
dist: {
|
||||
options: {
|
||||
patterns: [
|
||||
{
|
||||
match: 'googleAnalyticsId',
|
||||
replacement: '<%= settings.googleAnalyticsId %>'
|
||||
},
|
||||
{
|
||||
match: 'version',
|
||||
replacement: 'v<%= pkg.version %>'
|
||||
}
|
||||
]
|
||||
},
|
||||
files: [
|
||||
{expand: true, flatten: true, src: ['front/src/main.html'], dest: 'front/build/'}
|
||||
]
|
||||
}
|
||||
},
|
||||
jshint: {
|
||||
all: [
|
||||
'*.js',
|
||||
'app/lib/*.js',
|
||||
'bin/*.js',
|
||||
'lib/**/*.js',
|
||||
'app/nodeControllers/*.js',
|
||||
'app/public/scripts/*.js',
|
||||
'phantomas_custom/**/*.js',
|
||||
'test/api/*.js',
|
||||
'test/core/*.js',
|
||||
'test/fixtures/*.js',
|
||||
'front/src/js/**/*.js'
|
||||
]
|
||||
},
|
||||
clean: {
|
||||
tmp: {
|
||||
src: ['.tmp']
|
||||
},
|
||||
dev: {
|
||||
src: ['front/src/css']
|
||||
},
|
||||
coverage: {
|
||||
src: ['.tmp', 'coverage/']
|
||||
},
|
||||
build: {
|
||||
src: ['front/build']
|
||||
}
|
||||
},
|
||||
copy: {
|
||||
beforeCoverage: {
|
||||
files: [
|
||||
{src: ['bin/server.js'], dest: '.tmp/'}
|
||||
]
|
||||
},
|
||||
coverage: {
|
||||
files: [
|
||||
{cwd: 'test', src: '**/*', dest: 'coverage/test', expand: true},
|
||||
{cwd: 'lib/metadata', src: '**/*', dest: 'coverage/lib/metadata', expand: true},
|
||||
{cwd: 'node_modules/phantomas', src: '**/*', dest: 'coverage/node_modules/phantomas', expand: true},
|
||||
{cwd: 'lib/tools/phantomas/custom_modules', src: '**/*', dest: 'coverage/lib/tools/phantomas/custom_modules', expand: true}
|
||||
]
|
||||
},
|
||||
build: {
|
||||
files: [
|
||||
{src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
|
||||
{src: ['./front/src/img/logo-large.png'], dest: './front/build/img/logo-large.png'},
|
||||
]
|
||||
}
|
||||
},
|
||||
lineremover: {
|
||||
beforeCoverage: {
|
||||
files: {
|
||||
'.tmp/bin/cli.js': 'bin/cli.js'
|
||||
},
|
||||
options: {
|
||||
exclusionPattern: /#!\/usr\/bin\/env node/
|
||||
}
|
||||
}
|
||||
},
|
||||
blanket: {
|
||||
coverageApp: {
|
||||
src: ['app/'],
|
||||
dest: 'coverage/app/'
|
||||
},
|
||||
coverageLib: {
|
||||
src: ['lib/'],
|
||||
dest: 'coverage/lib/'
|
||||
},
|
||||
coverageBin: {
|
||||
src: ['.tmp/bin/'],
|
||||
dest: 'coverage/bin/'
|
||||
}
|
||||
},
|
||||
mochaTest: {
|
||||
test: {
|
||||
options: {
|
||||
reporter: 'spec',
|
||||
},
|
||||
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
|
||||
},
|
||||
'test-current-work': {
|
||||
options: {
|
||||
reporter: 'spec',
|
||||
},
|
||||
src: ['test/core/imageOptimizerTest.js']
|
||||
},
|
||||
coverage: {
|
||||
options: {
|
||||
reporter: 'html-cov',
|
||||
quiet: true,
|
||||
captureFile: 'coverage/coverage.html'
|
||||
},
|
||||
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
|
||||
}
|
||||
},
|
||||
env: {
|
||||
dev: {
|
||||
NODE_ENV: 'development'
|
||||
},
|
||||
built: {
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
},
|
||||
express: {
|
||||
dev: {
|
||||
options: {
|
||||
port: 8383,
|
||||
server: './bin/server.js',
|
||||
serverreload: true,
|
||||
showStack: true
|
||||
}
|
||||
},
|
||||
built: {
|
||||
options: {
|
||||
port: 8383,
|
||||
server: './bin/server.js',
|
||||
serverreload: true,
|
||||
showStack: true
|
||||
}
|
||||
},
|
||||
test: {
|
||||
options: {
|
||||
port: 8387,
|
||||
server: './coverage/bin/server.js',
|
||||
showStack: true
|
||||
}
|
||||
},
|
||||
'test-current-work': {
|
||||
options: {
|
||||
port: 8387,
|
||||
server: './bin/server.js',
|
||||
showStack: true
|
||||
}
|
||||
},
|
||||
testSuite: {
|
||||
options: {
|
||||
port: 8388,
|
||||
bases: 'test/www'
|
||||
}
|
||||
}
|
||||
},
|
||||
useminPrepare: {
|
||||
html: './front/src/main.html',
|
||||
options: {
|
||||
dest: './front/build',
|
||||
root: ['./', './front/src']
|
||||
}
|
||||
},
|
||||
usemin: {
|
||||
html: './front/build/main.html',
|
||||
css: './front/build/css/*.css',
|
||||
options: {
|
||||
assetsDirs: ['front/build']
|
||||
}
|
||||
},
|
||||
htmlmin: {
|
||||
options: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: true
|
||||
},
|
||||
main: {
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: './front/build/',
|
||||
src: 'main.html',
|
||||
flatten: true,
|
||||
dest: './front/build'
|
||||
}]
|
||||
},
|
||||
views: {
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: './front/src/views',
|
||||
src: '*.html',
|
||||
flatten: true,
|
||||
dest: '.tmp/views/'
|
||||
}]
|
||||
}
|
||||
},
|
||||
inline_angular_templates: {
|
||||
build: {
|
||||
options: {
|
||||
base: '.tmp',
|
||||
method: 'append'
|
||||
},
|
||||
files: {
|
||||
'./front/build/main.html': ['.tmp/views/*.html']
|
||||
}
|
||||
}
|
||||
},
|
||||
filerev: {
|
||||
options: {
|
||||
algorithm: 'md5',
|
||||
length: 8
|
||||
},
|
||||
assets: {
|
||||
src: './front/build/*/*.*'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Custom task: copies the test settings.json file to the coverage folder, and checks if there's no missing fields
|
||||
grunt.registerTask('copy-test-server-settings', function() {
|
||||
var mainSettingsFile = './server_config/settings.json';
|
||||
var testSettingsFile = './test/fixtures/settings.json';
|
||||
|
||||
var mainSettings = grunt.file.readJSON(mainSettingsFile);
|
||||
var testSettings = grunt.file.readJSON(testSettingsFile);
|
||||
|
||||
// Recursively compare keys of two objects (not the values)
|
||||
function compareKeys(original, copy, context) {
|
||||
for (var key in original) {
|
||||
if (!copy[key] && copy[key] !== '' && copy[key] !== 0) {
|
||||
grunt.fail.warn('Settings file ' + testSettingsFile + ' doesn\'t contain key ' + context + '.' + key);
|
||||
}
|
||||
if (original[key] !== null && typeof original[key] === 'object') {
|
||||
compareKeys(original[key], copy[key], context + '.' + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compareKeys(mainSettings, testSettings, 'settings');
|
||||
|
||||
var outputFile = './coverage/server_config/settings.json';
|
||||
grunt.file.write(outputFile, JSON.stringify(testSettings, null, 4));
|
||||
grunt.log.ok('File ' + outputFile + ' created');
|
||||
});
|
||||
|
||||
|
||||
grunt.registerTask('icons', [
|
||||
'webfont:icons',
|
||||
'less',
|
||||
'clean:tmp'
|
||||
]);
|
||||
|
||||
grunt.registerTask('build', [
|
||||
'jshint',
|
||||
'clean:build',
|
||||
'copy:build',
|
||||
'less',
|
||||
'useminPrepare',
|
||||
'concat',
|
||||
'uglify',
|
||||
'cssmin',
|
||||
'replace',
|
||||
'htmlmin:views',
|
||||
'inline_angular_templates',
|
||||
'filerev',
|
||||
'usemin',
|
||||
'htmlmin:main',
|
||||
'clean:tmp'
|
||||
]);
|
||||
|
||||
grunt.registerTask('hint', [
|
||||
'jshint'
|
||||
]);
|
||||
|
||||
grunt.registerTask('dev', [
|
||||
'env:dev',
|
||||
'express:dev'
|
||||
]);
|
||||
|
||||
grunt.registerTask('built', [
|
||||
'env:built',
|
||||
'express:built'
|
||||
]);
|
||||
|
||||
grunt.registerTask('test', [
|
||||
'build',
|
||||
'express:testSuite',
|
||||
'clean:coverage',
|
||||
'copy-test-server-settings',
|
||||
'lineremover:beforeCoverage',
|
||||
'copy:beforeCoverage',
|
||||
'blanket',
|
||||
'copy:coverage',
|
||||
'express:test',
|
||||
'mochaTest:test',
|
||||
'mochaTest:coverage',
|
||||
'clean:tmp'
|
||||
]);
|
||||
|
||||
grunt.registerTask('test-current-work', [
|
||||
'jshint',
|
||||
'express:testSuite',
|
||||
'clean:coverage',
|
||||
'copy-test-server-settings',
|
||||
'express:test-current-work',
|
||||
'mochaTest:test-current-work',
|
||||
'clean:tmp'
|
||||
]);
|
||||
|
||||
};
|
||||
@@ -11,15 +11,25 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>online tool</b> (recommended):
|
||||
<a href="http://yellowlab.tools" target="_blank">http://yellowlab.tools</a>
|
||||
<a href="https://yellowlab.tools" target="_blank">https://yellowlab.tools</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/YLT-animated.gif"></img>
|
||||
<img src="./doc/img/home.png"></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>
|
||||
The <b>Docker image</b> - <a href="https://github.com/ousamabenyounes/docker-yellowlabtools" target="_blank">ousamabenyounes/docker-yellowlabtools</a>
|
||||
<br>
|
||||
Your own private instance of Yellow Lab Tools, on your computer.
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/docker-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/YLT-cli-animated.gif"></img>
|
||||
@@ -27,16 +37,7 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
|
||||
</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>
|
||||
The <b>NodeJS</b> package - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/npm-logo.png"></img>
|
||||
@@ -44,7 +45,7 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
|
||||
</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>
|
||||
The <b>public API</b> - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
|
||||
<br>
|
||||
Allows you to launch runs from any language
|
||||
</td>
|
||||
@@ -58,32 +59,33 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
|
||||
|
||||
## 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 then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
|
||||
The tool loads the given URL 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!!!
|
||||
By the way, it's free because I am a geek, not businessmen. In return, you can add a ★ on GitHub, it will boost my motivation to add more awesome features!!!
|
||||
|
||||

|
||||
|
||||
|
||||
## Test your localhost
|
||||
|
||||
You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel between your localhost and the online tool (or the public API). You can also use the CLI or the Grunt tasks as they run on your machine.
|
||||
|
||||
|
||||
## Install your own private instance
|
||||
|
||||
If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-private-server).
|
||||
If your project is not accessible from outside or if you want to test your localhost, you might want to run your own instance of Yellow Lab Tools.
|
||||
|
||||
The classical way is to clone the YLT server's GitHub repository and run it on Linux or MacOS. The documentation is [here](https://github.com/YellowLabTools/YellowLabTools/wiki/Install-your-private-server).
|
||||
|
||||
The new recommended solution is to run Yellow Lab Tools inside a Docker virtual machine. My friend Ousama Ben Younes maintains [this ready-to-use Docker image based on Alpine](https://github.com/ousamabenyounes/docker-yellowlabtools)).
|
||||
|
||||
|
||||
## Author
|
||||
Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of YellowLabTools!
|
||||
Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of Yellow Lab Tools!
|
||||
|
||||
If you understand French, you can visit [my website](http://www.gaelmetais.com) (will be soon in English too).
|
||||
<a href='https://ko-fi.com/gaelmetais' target='_blank'><img height='35' style='border:0px;height:46px;' src='https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0' border='0' alt='Buy me a coffee' /><a>
|
||||
|
||||
I can also help your company about Web Performances, visit [my website](https://www.gaelmetais.com).
|
||||
|
||||
|
||||
## Contributors
|
||||
- Achraf Ben Younes [achrafbenyounes](https://github.com/achrafbenyounes)
|
||||
- [camlafit](https://github.com/camlafit)
|
||||
- Km [camlafit](https://github.com/camlafit)
|
||||
- Vincent L. [magikcypress](https://github.com/magikcypress)
|
||||
- Ousama Ben Younes [ousamabenyounes](https://github.com/ousamabenyounes)
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
Vagrant.configure("2") do |config|
|
||||
|
||||
config.vm.box = "ubuntu/precise64"
|
||||
|
||||
config.vm.network :private_network, ip: "10.10.10.10"
|
||||
config.ssh.forward_agent = true
|
||||
|
||||
# http://foo-o-rama.com/vagrant--stdin-is-not-a-tty--fix.html
|
||||
config.vm.provision "fix-no-tty", type: "shell" do |s|
|
||||
s.privileged = false
|
||||
s.inline = "sudo sed -i '/tty/!s/mesg n/tty -s \\&\\& mesg n/' /root/.profile"
|
||||
end
|
||||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
|
||||
vb.customize ["modifyvm", :id, "--memory", 1024]
|
||||
vb.customize ["modifyvm", :id, "--cpus", 2]
|
||||
end
|
||||
|
||||
config.vm.provision :shell, :path => "server_config/server_install.sh"
|
||||
|
||||
end
|
||||
@@ -13,16 +13,21 @@ var cli = meow({
|
||||
' yellowlabtools <url> <options>',
|
||||
'',
|
||||
'Options:',
|
||||
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
|
||||
' --device Simulates a device. Choose between phone (default), tablet, desktop and desktop-hd.',
|
||||
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
|
||||
' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
|
||||
' --proxy Sets an HTTP proxy to pass through. Syntax is "host:port".',
|
||||
' --cookie Adds a cookie on the main domain.',
|
||||
' --auth-user Basic HTTP authentication username.',
|
||||
' --auth-pass Basic HTTP authentication password.',
|
||||
' --block-domain Disallow requests to given (comma-separated) domains.',
|
||||
' --allow-domain Only allow requests to given (comma-separated) domains.',
|
||||
' --no-externals Block all domains except the main one.',
|
||||
' --reporter The output format: "json" or "xml". Default is "json".',
|
||||
' --local-storage Ability to set a local storage, key-value pairs (e.g. "bar=foo;domain=url")',
|
||||
' --session-storage Ability to set a session storage, key-value pairs (e.g. "bar=foo;domain=url")',
|
||||
''
|
||||
].join('\n'),
|
||||
pkg: '../package.json'
|
||||
pkg: require('../package.json')
|
||||
});
|
||||
|
||||
|
||||
@@ -51,11 +56,14 @@ if (screenshot) {
|
||||
}
|
||||
|
||||
// Device simulation
|
||||
options.device = cli.flags.device || 'desktop';
|
||||
options.device = cli.flags.device || 'mobile';
|
||||
|
||||
// Wait for CSS selector
|
||||
options.waitForSelector = cli.flags.waitForSelector || null;
|
||||
|
||||
// Proxy
|
||||
options.proxy = cli.flags.proxy || null;
|
||||
|
||||
// Cookie
|
||||
options.cookie = cli.flags.cookie || null;
|
||||
|
||||
@@ -63,6 +71,15 @@ options.cookie = cli.flags.cookie || null;
|
||||
options.authUser = cli.flags.authUser || null;
|
||||
options.authPass = cli.flags.authPass || null;
|
||||
|
||||
// Domain blocking
|
||||
options.blockDomain = cli.flags.blockDomain || null;
|
||||
options.allowDomain = cli.flags.allowDomain || null;
|
||||
options.noExternals = cli.flags.noExternals || null;
|
||||
|
||||
// Storage injection
|
||||
options.localStorage = cli.flags.localStorage;
|
||||
options.sessionStorage = cli.flags.sessionStorage;
|
||||
|
||||
// Output format
|
||||
if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !== 'xml') {
|
||||
console.error('Incorrect parameters: reporter has to be "json" or "xml"');
|
||||
@@ -80,8 +97,27 @@ if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !=
|
||||
debug('Success');
|
||||
switch(cli.flags.reporter) {
|
||||
case 'xml':
|
||||
var serializer = new EasyXml();
|
||||
console.log(serializer.render(data));
|
||||
var serializer = new EasyXml({
|
||||
manifest: true
|
||||
});
|
||||
|
||||
// Remove some heavy parts of the results object
|
||||
delete data.toolsResults;
|
||||
|
||||
var xmlOutput = serializer.render(data);
|
||||
|
||||
// Remove special chars from XML tags: # [ ]
|
||||
xmlOutput = xmlOutput.replace(/<([^>]*)#([^>]*)>/g, '<$1>');
|
||||
xmlOutput = xmlOutput.replace(/<([^>]*)\[([^>]*)>/g, '<$1>');
|
||||
xmlOutput = xmlOutput.replace(/<([^>]*)\]([^>]*)>/g, '<$1>');
|
||||
|
||||
// Remove special chars from text content: \n \0
|
||||
xmlOutput = xmlOutput.replace(/(<[a-zA-Z]*>[^<]*)\n([^<]*<\/[a-zA-Z]*>)/g, '$1$2');
|
||||
xmlOutput = xmlOutput.replace(/\0/g, '');
|
||||
xmlOutput = xmlOutput.replace(/\uFFFF/g, '');
|
||||
xmlOutput = xmlOutput.replace(/\u0002/g, '');
|
||||
|
||||
console.log(xmlOutput);
|
||||
break;
|
||||
default:
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
var bodyParser = require('body-parser');
|
||||
var compress = require('compression');
|
||||
var cors = require('cors');
|
||||
|
||||
var authMiddleware = require('../lib/server/middlewares/authMiddleware');
|
||||
var apiLimitsMiddleware = require('../lib/server/middlewares/apiLimitsMiddleware');
|
||||
var wwwRedirectMiddleware = require('../lib/server/middlewares/wwwRedirectMiddleware');
|
||||
|
||||
|
||||
// Middlewares
|
||||
app.use(compress());
|
||||
app.use(bodyParser.json());
|
||||
app.use(cors());
|
||||
app.use(wwwRedirectMiddleware);
|
||||
app.use(authMiddleware);
|
||||
app.use(apiLimitsMiddleware);
|
||||
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
|
||||
|
||||
|
||||
// Initialize the controllers
|
||||
var apiController = require('../lib/server/controllers/apiController')(app);
|
||||
var frontController = require('../lib/server/controllers/frontController')(app);
|
||||
|
||||
|
||||
// Let's start the server!
|
||||
if (!process.env.GRUNTED) {
|
||||
var settings = require('../server_config/settings.json');
|
||||
server.listen(settings.serverPort, function() {
|
||||
console.log('Listening on port %d', server.address().port);
|
||||
|
||||
// For the tests
|
||||
if (server.startTests) {
|
||||
server.startTests();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = app;
|
||||
|
Before Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 45 KiB |
@@ -1,10 +0,0 @@
|
||||
.about {
|
||||
margin: 3em auto;
|
||||
width: 50%;
|
||||
}
|
||||
.about p {
|
||||
margin: 2em;
|
||||
}
|
||||
.about a {
|
||||
color: #FFF;
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
.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 > a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.summary .notations .criteria .table > a:hover > div {
|
||||
background: #EBD8E2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.summary .notations .criteria .table > a:hover > div.info {
|
||||
background: #FFF;
|
||||
}
|
||||
.summary .notations .criteria .table > a: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;
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
.promess {
|
||||
padding: 0em 2em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.price {
|
||||
padding: 0em 2em 3em;
|
||||
margin-top: 0em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.url {
|
||||
width: 50%;
|
||||
}
|
||||
.launchBtn {
|
||||
background: #ffa319;
|
||||
color: #fff;
|
||||
}
|
||||
.launchBtn:focus {
|
||||
background: #e74c3c;
|
||||
}
|
||||
.launchBtn.disabled {
|
||||
background: #deaca6;
|
||||
}
|
||||
.launchBtn.disabled:focus {
|
||||
color: #ddd;
|
||||
}
|
||||
.settings {
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.settings input,
|
||||
.settings select {
|
||||
font-size: 1em;
|
||||
}
|
||||
.settings input[type=text],
|
||||
.settings input[type=password],
|
||||
.settings textarea {
|
||||
width: 100%;
|
||||
min-width: 4em;
|
||||
}
|
||||
.device {
|
||||
margin-top: 3em;
|
||||
}
|
||||
.device .item {
|
||||
display: inline-block;
|
||||
margin: 1em 0.75em;
|
||||
width: 5.5em;
|
||||
height: 5.5em;
|
||||
color: #FFF;
|
||||
border: 1px solid #FFF;
|
||||
padding: 1px;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.device .item.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;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.advanced .subTable {
|
||||
display: table;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.advanced .subTable > div {
|
||||
display: table-row;
|
||||
}
|
||||
.advanced .subTable > div > div {
|
||||
display: table-cell;
|
||||
padding: 0 0 0.75em;
|
||||
}
|
||||
.features {
|
||||
display: table;
|
||||
width: 50%;
|
||||
margin: 6em auto 0;
|
||||
font-size: 0.9em;
|
||||
color: #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;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.status {
|
||||
margin-top: 2em;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
.statusSubMessage {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 6em;
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
.rule.board {
|
||||
text-align: center;
|
||||
}
|
||||
.rule .ruleTable {
|
||||
display: table;
|
||||
border-spacing: 1em;
|
||||
width: 90%;
|
||||
margin: 2em auto;
|
||||
background: #f2f2f2;
|
||||
border: 1px dashed #666;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
.rule .ruleTable > div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.rule .ruleTable .left {
|
||||
width: 33%;
|
||||
font-weight: bold;
|
||||
}
|
||||
.rule .ruleTable .right {
|
||||
width: 67%;
|
||||
}
|
||||
.rule .score {
|
||||
font-size: 2.5em;
|
||||
line-height: 2em;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0 auto 0.5em;
|
||||
}
|
||||
.rule h3 {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
.rule .okThreshold {
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.rule .message {
|
||||
width: 80%;
|
||||
margin: 1.5em auto;
|
||||
}
|
||||
.rule .message p {
|
||||
margin: 0.5em;
|
||||
}
|
||||
.rule .message ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
.rule .message li:before {
|
||||
content: '\25e6';
|
||||
margin-right: 0.3em;
|
||||
font-size: 1.2em;
|
||||
position: relative;
|
||||
top: 0.1em;
|
||||
}
|
||||
.rule .warning {
|
||||
width: 90%;
|
||||
margin: -1em auto 2em;
|
||||
background: #FEE;
|
||||
border: 1px dashed #e74c3c;
|
||||
color: #e74c3c;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
.rule .offendersTable {
|
||||
display: table;
|
||||
border-spacing: 0 0.25em;
|
||||
margin: 0 auto;
|
||||
min-width: 10%;
|
||||
max-width: 90%;
|
||||
}
|
||||
.rule .offendersTable > div {
|
||||
display: table-row;
|
||||
}
|
||||
.rule .offendersTable > div > div {
|
||||
display: table-cell;
|
||||
background: #f2f2f2;
|
||||
padding: 0 1em;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
.rule .offendersTable > div > div:hover {
|
||||
background: #EBD8E2;
|
||||
}
|
||||
.rule .notFound {
|
||||
font-size: 1em;
|
||||
}
|
||||
.rule .notFound h2 {
|
||||
font-size: 3em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.rule .startTime {
|
||||
display: none;
|
||||
}
|
||||
.offendersTable .offenderButton,
|
||||
.value .offenderButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #efe;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.offendersTable .offenderButton.opens,
|
||||
.value .offenderButton.opens {
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
.offendersTable .offenderButton.opens:after,
|
||||
.value .offenderButton.opens:after {
|
||||
position: relative;
|
||||
left: 0.5em;
|
||||
content: '\25BC';
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.offendersTable .offenderButton > div,
|
||||
.value .offenderButton > div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: 100%;
|
||||
background: inherit;
|
||||
border-bottom-left-radius: 0.4em;
|
||||
border-bottom-right-radius: 0.4em;
|
||||
border-top: 1px solid #999;
|
||||
z-index: 2;
|
||||
}
|
||||
.offendersTable .offenderButton .domTree,
|
||||
.value .offenderButton .domTree {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.offendersTable .offenderButton .domTree > div,
|
||||
.value .offenderButton .domTree > div {
|
||||
margin: 0.5em;
|
||||
}
|
||||
.offendersTable .offenderButton .domTree > div div,
|
||||
.value .offenderButton .domTree > div div {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.offendersTable .offenderButton .backtrace,
|
||||
.value .offenderButton .backtrace,
|
||||
.offendersTable .offenderButton .cssFileAndLine,
|
||||
.value .offenderButton .cssFileAndLine {
|
||||
white-space: nowrap;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.offendersTable .offenderButton.opens:hover,
|
||||
.value .offenderButton.opens:hover {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: #ffe0cc;
|
||||
z-index: 2;
|
||||
}
|
||||
.offendersTable .offenderButton.opens:hover > div,
|
||||
.value .offenderButton.opens:hover > div {
|
||||
display: block;
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.offendersTable .smallerOffenders,
|
||||
.value .smallerOffenders {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.offendersHtml {
|
||||
display: inline-block;
|
||||
}
|
||||
.domTree div {
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.domTree div span:only-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
.domTree div span:only-child span {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
.checker {
|
||||
/* Checkerboard background */
|
||||
background-color: #ddd;
|
||||
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
|
||||
background-size: 1em 1em;
|
||||
background-position: 0 0, 0.5em 0.5em;
|
||||
}
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
}
|
||||
.colorPalette > div {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
}
|
||||
.colorPalette > div div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
background: #FFF;
|
||||
padding: 0.5em;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
white-space: nowrap;
|
||||
z-index: 3;
|
||||
font-weight: bold;
|
||||
}
|
||||
.colorPalette > div:hover div {
|
||||
display: block;
|
||||
}
|
||||
.colorPalette > div:hover:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
left: -0.2em;
|
||||
top: -0.2em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
.similarColors {
|
||||
margin: 1em;
|
||||
width: 20em;
|
||||
height: 6em;
|
||||
}
|
||||
.similarColors > div {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
height: 3.5em;
|
||||
padding-top: 2.5em;
|
||||
}
|
||||
.totalWeightPie {
|
||||
max-width: 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;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.screenshot.board {
|
||||
text-align: center;
|
||||
}
|
||||
.screenshot .screenshotWrapper {
|
||||
font-size: 2.08333333333333em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
/* 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,
|
||||
.table > a {
|
||||
display: table-row;
|
||||
}
|
||||
.table > .headers > div {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
.table > div > div,
|
||||
.table > a > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<?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: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<?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: 634 B |
@@ -1 +0,0 @@
|
||||
<?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: 792 B |
@@ -1 +0,0 @@
|
||||
<?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: 679 B |
@@ -1 +0,0 @@
|
||||
<?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: 875 B |
@@ -1 +0,0 @@
|
||||
<?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: 851 B |
@@ -1 +0,0 @@
|
||||
<?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: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<?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: 584 B |
@@ -1 +0,0 @@
|
||||
<?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: 774 B |
@@ -1 +0,0 @@
|
||||
<?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: 887 B |
|
Before Width: | Height: | Size: 794 B |
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,88 +0,0 @@
|
||||
var yltApp = angular.module('YellowLabTools', [
|
||||
'ngRoute',
|
||||
'ngSanitize',
|
||||
'ngAnimate',
|
||||
'indexCtrl',
|
||||
'dashboardCtrl',
|
||||
'queueCtrl',
|
||||
'ruleCtrl',
|
||||
'screenshotCtrl',
|
||||
'timelineCtrl',
|
||||
'runsFactory',
|
||||
'resultsFactory',
|
||||
'apiService',
|
||||
'menuService',
|
||||
'settingsService',
|
||||
'gradeDirective',
|
||||
'offendersDirectives',
|
||||
'LocalStorageModule'
|
||||
]);
|
||||
|
||||
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
|
||||
$rootScope.isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent);
|
||||
$rootScope.loadedRunId = null;
|
||||
|
||||
var oldHash;
|
||||
|
||||
// We don't want the hash to be kept between two pages
|
||||
$rootScope.$on('$locationChangeStart', function(param1, param2, param3, param4){
|
||||
var newHash = $location.hash();
|
||||
if (newHash === oldHash) {
|
||||
$location.hash(null);
|
||||
}
|
||||
oldHash = newHash;
|
||||
});
|
||||
|
||||
// Google Analytics
|
||||
$rootScope.$on('$routeChangeSuccess', function(){
|
||||
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',
|
||||
function($routeProvider, $locationProvider) {
|
||||
$routeProvider.
|
||||
when('/', {
|
||||
templateUrl: 'views/index.html',
|
||||
controller: 'IndexCtrl'
|
||||
}).
|
||||
when('/queue/:runId', {
|
||||
templateUrl: 'views/queue.html',
|
||||
controller: 'QueueCtrl'
|
||||
}).
|
||||
when('/about', {
|
||||
templateUrl: 'views/about.html'
|
||||
}).
|
||||
when('/result/:runId', {
|
||||
templateUrl: 'views/dashboard.html',
|
||||
controller: 'DashboardCtrl'
|
||||
}).
|
||||
when('/result/:runId/timeline', {
|
||||
templateUrl: 'views/timeline.html',
|
||||
controller: 'TimelineCtrl'
|
||||
}).
|
||||
when('/result/:runId/screenshot', {
|
||||
templateUrl: 'views/screenshot.html',
|
||||
controller: 'ScreenshotCtrl'
|
||||
}).
|
||||
when('/result/:runId/rule/:policy', {
|
||||
templateUrl: 'views/rule.html',
|
||||
controller: 'RuleCtrl'
|
||||
}).
|
||||
otherwise({
|
||||
redirectTo: '/'
|
||||
});
|
||||
|
||||
$locationProvider.html5Mode(true);
|
||||
}
|
||||
]);
|
||||
|
||||
// Disable debugging https://docs.angularjs.org/guide/production
|
||||
yltApp.config(['$compileProvider', function ($compileProvider) {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
}]);
|
||||
@@ -1,60 +0,0 @@
|
||||
var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
|
||||
|
||||
dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
|
||||
$scope.runId = $routeParams.runId;
|
||||
$scope.Menu = Menu.setCurrentPage('dashboard', $scope.runId);
|
||||
$scope.fromSocialShare = $location.search().share;
|
||||
|
||||
function loadResults() {
|
||||
// Load result if needed
|
||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
||||
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
init();
|
||||
}, function(err) {
|
||||
$scope.error = true;
|
||||
});
|
||||
} else {
|
||||
$scope.result = $rootScope.loadedResult;
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
// By default, Angular sorts object's attributes alphabetically. Countering this problem by retrieving the keys order here.
|
||||
$scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
|
||||
|
||||
$scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
|
||||
|
||||
$scope.tweetText = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
|
||||
}
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
// 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?text=' + encodeURIComponent(message + 'http://yellowlab.tools'));
|
||||
};
|
||||
|
||||
$scope.shareOnLinkedin = function(message) {
|
||||
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) {
|
||||
var winHeight = 400;
|
||||
var winWidth = 600;
|
||||
var winTop = (screen.height / 2) - (winHeight / 2);
|
||||
var winLeft = (screen.width / 2) - (winWidth / 2);
|
||||
window.open(url, 'sharer', 'top=' + winTop + ',left=' + winLeft + ',toolbar=0,status=0,width=' + winWidth + ',height=' + winHeight);
|
||||
}
|
||||
|
||||
loadResults();
|
||||
}]);
|
||||
@@ -1,13 +0,0 @@
|
||||
var indexCtrl = angular.module('indexCtrl', []);
|
||||
|
||||
indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) {
|
||||
|
||||
$scope.settings = Settings.getMergedSettings();
|
||||
|
||||
$scope.launchTest = function() {
|
||||
if ($scope.url) {
|
||||
Settings.saveSettings($scope.settings);
|
||||
API.launchTest($scope.url, $scope.settings);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -1,42 +0,0 @@
|
||||
var queueCtrl = angular.module('queueCtrl', ['runsFactory']);
|
||||
|
||||
queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', 'API', function($scope, $routeParams, $location, Runs, API) {
|
||||
$scope.runId = $routeParams.runId;
|
||||
|
||||
var numberOfTries = 0;
|
||||
|
||||
function getRunStatus () {
|
||||
Runs.get({runId: $scope.runId}, function(data) {
|
||||
$scope.url = data.params.url;
|
||||
$scope.status = data.status;
|
||||
$scope.notFound = false;
|
||||
$scope.connectionLost = false;
|
||||
|
||||
if (data.status.statusCode === 'running' || data.status.statusCode === 'awaiting') {
|
||||
numberOfTries ++;
|
||||
|
||||
// Retrying in 2 seconds (and increasing the delay a bit more each time)
|
||||
setTimeout(getRunStatus, 2000 + (numberOfTries * 100));
|
||||
|
||||
} else if (data.status.statusCode === 'complete') {
|
||||
$location.path('/result/' + $scope.runId).replace();
|
||||
} else {
|
||||
// Handled by the view
|
||||
}
|
||||
}, function(response) {
|
||||
if (response.status === 404) {
|
||||
$scope.notFound = true;
|
||||
$scope.connectionLost = false;
|
||||
} else if (response.status === 0) {
|
||||
// Connection lost, retry in 10 seconds
|
||||
setTimeout(getRunStatus, 10000);
|
||||
$scope.connectionLost = true;
|
||||
$scope.notFound = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getRunStatus();
|
||||
}]);
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
var ruleCtrl = angular.module('ruleCtrl', ['chart.js']);
|
||||
|
||||
ruleCtrl.config(['ChartJsProvider', function (ChartJsProvider) {
|
||||
// Configure all charts
|
||||
ChartJsProvider.setOptions({
|
||||
animation: false,
|
||||
colours: ['#FF5252', '#FF8A80'],
|
||||
responsive: true
|
||||
});
|
||||
}]);
|
||||
|
||||
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, API) {
|
||||
$scope.runId = $routeParams.runId;
|
||||
$scope.policyName = $routeParams.policy;
|
||||
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
|
||||
$scope.rule = null;
|
||||
|
||||
function loadResults() {
|
||||
// Load result if needed
|
||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
||||
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
init();
|
||||
});
|
||||
} else {
|
||||
$scope.result = $rootScope.loadedResult;
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
$scope.rule = $scope.result.rules[$scope.policyName];
|
||||
|
||||
// Init "Total Weight" chart
|
||||
if ($scope.policyName === 'totalWeight') {
|
||||
$scope.weightLabels = [];
|
||||
$scope.weightColours = ['#7ECCCC', '#A7E846', '#FF944D', '#FFE74A', '#C2A3FF', '#5A9AED', '#FF6452', '#C1C1C1'];
|
||||
$scope.weightData = [];
|
||||
|
||||
var types = ['html', 'css', 'js', 'json', 'image', 'video', 'webfont', 'other'];
|
||||
types.forEach(function(type) {
|
||||
$scope.weightLabels.push(type);
|
||||
$scope.weightData.push(Math.round($scope.rule.offendersObj.list.byType[type].totalWeight / 1024));
|
||||
});
|
||||
|
||||
$scope.weightOptions = {
|
||||
tooltipTemplate: '<%=label%>: <%=value%> KB'
|
||||
};
|
||||
}
|
||||
|
||||
// Init "Breakpoints" chart
|
||||
if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
|
||||
|
||||
// Seek for the biggest breakpoint
|
||||
var max = 0;
|
||||
$scope.rule.offendersObj.forEach(function(offender) {
|
||||
if (offender.pixels > max) {
|
||||
max = offender.pixels;
|
||||
}
|
||||
});
|
||||
max = Math.max(max + 100, 1400);
|
||||
|
||||
// We group offenders 10px by 10px
|
||||
var GROUP_SIZE = 20;
|
||||
|
||||
// Generate an empty array of values
|
||||
$scope.breakpointsLabels = [];
|
||||
$scope.breakpointsData = [[]];
|
||||
for (var i = 0; i <= max / GROUP_SIZE; i++) {
|
||||
$scope.breakpointsLabels[i] = '';
|
||||
$scope.breakpointsData[0][i] = 0;
|
||||
}
|
||||
|
||||
// Fill it with results
|
||||
$scope.rule.offendersObj.forEach(function(offender) {
|
||||
var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
|
||||
|
||||
if ($scope.breakpointsLabels[group] !== '') {
|
||||
$scope.breakpointsLabels[group] += '/';
|
||||
}
|
||||
$scope.breakpointsLabels[group] += offender.breakpoint;
|
||||
|
||||
$scope.breakpointsData[0][group] += offender.count;
|
||||
});
|
||||
|
||||
$scope.breakpointsSeries = ['Number of CSS rules per breakpoint'];
|
||||
$scope.breakpointsColours = ['#9c4274'];
|
||||
$scope.breakpointsOptions = {
|
||||
scaleShowGridLines: false,
|
||||
barShowStroke: false,
|
||||
showTooltips: false,
|
||||
pointDot: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
strokeColor: 'rgba(20, 200, 20, 1)',
|
||||
scaleFontSize: 9
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
$location.path('/result/' + $scope.runId);
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
}]);
|
||||
@@ -1,30 +0,0 @@
|
||||
var screenshotCtrl = angular.module('screenshotCtrl', ['resultsFactory', 'menuService']);
|
||||
|
||||
screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
|
||||
$scope.runId = $routeParams.runId;
|
||||
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
|
||||
|
||||
function loadResults() {
|
||||
// Load result if needed
|
||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
||||
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
}, function(err) {
|
||||
$scope.error = true;
|
||||
});
|
||||
} else {
|
||||
$scope.result = $rootScope.loadedResult;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
$location.path('/result/' + $scope.runId);
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
}]);
|
||||
@@ -1,190 +0,0 @@
|
||||
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.totalRequests.offendersObj.list.byType.js;
|
||||
$scope.scripts = [];
|
||||
|
||||
offenders.forEach(function(filePath) {
|
||||
var shortPath = filePath;
|
||||
|
||||
if (filePath.length > 100) {
|
||||
shortPath = filePath.substr(0, 98) + '...';
|
||||
}
|
||||
|
||||
var scriptObj = {
|
||||
fullPath: filePath,
|
||||
shortPath: shortPath
|
||||
};
|
||||
|
||||
$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);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -1,33 +0,0 @@
|
||||
var gradeDirective = angular.module('gradeDirective', []);
|
||||
|
||||
gradeDirective.directive('grade', function() {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
score: '=score'
|
||||
},
|
||||
template: '<div ng-class="getGrade(score)">{{getGrade(score)}}</div>',
|
||||
replace: true,
|
||||
controller : ['$scope', function($scope) {
|
||||
$scope.getGrade = function(score) {
|
||||
if (score > 80) {
|
||||
return 'A';
|
||||
}
|
||||
if (score > 60) {
|
||||
return 'B';
|
||||
}
|
||||
if (score > 40) {
|
||||
return 'C';
|
||||
}
|
||||
if (score > 20) {
|
||||
return 'D';
|
||||
}
|
||||
if (score > 0) {
|
||||
return 'E';
|
||||
}
|
||||
return 'F';
|
||||
};
|
||||
}]
|
||||
};
|
||||
});
|
||||
@@ -1,921 +0,0 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
var offendersDirectives = angular.module('offendersDirectives', []);
|
||||
|
||||
function getdomTreeHTML(tree) {
|
||||
return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
|
||||
}
|
||||
|
||||
function getdomTreeInnerHTML(tree) {
|
||||
return recursiveHtmlBuilder(tree);
|
||||
}
|
||||
|
||||
function recursiveHtmlBuilder(tree) {
|
||||
var html = '';
|
||||
var keys = Object.keys(tree);
|
||||
|
||||
keys.forEach(function(key) {
|
||||
if (isNaN(tree[key])) {
|
||||
html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
|
||||
} else if (tree[key] > 1) {
|
||||
html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
|
||||
} else {
|
||||
html += '<div><span>' + key + '</span></div>';
|
||||
}
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
offendersDirectives.directive('domTree', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
tree: '='
|
||||
},
|
||||
template: '<div class="domTree"></div>',
|
||||
replace: true,
|
||||
link: function(scope, element) {
|
||||
element.append(getdomTreeInnerHTML(scope.tree));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function getDomElementButtonHTML(obj, onASingleLine) {
|
||||
if (obj.tree && !onASingleLine) {
|
||||
return '<div class="offenderButton opens">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
|
||||
} else {
|
||||
return '<div class="offenderButton">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function getDomElementButtonInnerHTML(obj, onASingleLine) {
|
||||
if (obj.type === 'html' ||
|
||||
obj.type === 'body' ||
|
||||
obj.type === 'head' ||
|
||||
obj.type === 'window' ||
|
||||
obj.type === 'document' ||
|
||||
obj.type === 'fragment') {
|
||||
return obj.type;
|
||||
}
|
||||
|
||||
if (obj.type === 'notAnElement') {
|
||||
return 'Incorrect element';
|
||||
}
|
||||
|
||||
var html = '';
|
||||
if (obj.type === 'domElement') {
|
||||
html = 'DOM element <b>' + obj.element + '</b>';
|
||||
} else if (obj.type === 'fragmentElement') {
|
||||
html = 'Fragment element <b>' + obj.element + '</b>';
|
||||
} else if (obj.type === 'createdElement') {
|
||||
html = 'Created element <b>' + obj.element + '</b>';
|
||||
}
|
||||
|
||||
if (obj.tree && !onASingleLine) {
|
||||
html += getdomTreeHTML(obj.tree);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
offendersDirectives.directive('domElementButton', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
obj: '='
|
||||
},
|
||||
template: '<div class="offenderButton" ng-class="{opens: obj.tree}"></div>',
|
||||
replace: true,
|
||||
link: function(scope, element) {
|
||||
element.append(getDomElementButtonInnerHTML(scope.obj));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
function getJQueryContextButtonHTML(context, onASingleLine) {
|
||||
if (context.length === 0) {
|
||||
return '<span class="offenderButton">Empty jQuery object</span>';
|
||||
}
|
||||
|
||||
if (context.length === 1) {
|
||||
return getDomElementButtonHTML(context.elements[0], onASingleLine);
|
||||
}
|
||||
|
||||
var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine);
|
||||
if (context.length === 3) {
|
||||
html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine);
|
||||
} else if (context.length > 3) {
|
||||
html += ' and ' + (context.length - 2) + ' more...';
|
||||
}
|
||||
return html + ')';
|
||||
}
|
||||
|
||||
function isJQuery(node) {
|
||||
return node.data.type.indexOf('jQuery ') === 0;
|
||||
}
|
||||
|
||||
function getNonJQueryHTML(node, onASingleLine) {
|
||||
var type = node.data.type;
|
||||
|
||||
if (node.windowPerformance) {
|
||||
switch (type) {
|
||||
case 'documentScroll':
|
||||
return '(triggering the scroll event on <b>document</b>)';
|
||||
|
||||
case 'windowScroll':
|
||||
return '(triggering the scroll event on <b>window</b>)';
|
||||
|
||||
case 'window.onscroll':
|
||||
return '(calling the <b>window.onscroll</b> function)';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!node.data.callDetails) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var args = node.data.callDetails.arguments;
|
||||
var ctxt = node.data.callDetails.context;
|
||||
|
||||
|
||||
switch (type) {
|
||||
case 'getElementById':
|
||||
case 'createElement':
|
||||
return '<b>' + args[0] + '</b>';
|
||||
|
||||
case 'getElementsByClassName':
|
||||
case 'getElementsByTagName':
|
||||
case 'querySelector':
|
||||
case 'querySelectorAll':
|
||||
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
|
||||
|
||||
case 'appendChild':
|
||||
return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
|
||||
|
||||
case 'insertBefore':
|
||||
return 'insert ' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine);
|
||||
|
||||
case 'addEventListener':
|
||||
return 'bind <b>' + args[0] + '</b> to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
|
||||
|
||||
case 'getComputedStyle':
|
||||
return getDomElementButtonHTML(args[0], onASingleLine) + (args[1] || '');
|
||||
|
||||
case 'error':
|
||||
return args[0];
|
||||
|
||||
case 'jQuery - onDOMReady':
|
||||
return '(function)';
|
||||
|
||||
case 'documentScroll':
|
||||
return 'The scroll event just triggered on document';
|
||||
|
||||
case 'windowScroll':
|
||||
return 'The scroll event just triggered on window';
|
||||
|
||||
case 'window.onscroll':
|
||||
return 'The window.onscroll function just got called';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getJQueryHTML(node, onASingleLine) {
|
||||
var type = node.data.type;
|
||||
var unescapedArgs = node.data.callDetails.arguments;
|
||||
var args = [];
|
||||
var ctxt = node.data.callDetails.context;
|
||||
|
||||
// escape HTML in args
|
||||
for (var i = 0 ; i < 4 ; i ++) {
|
||||
if (unescapedArgs[i] !== undefined) {
|
||||
args[i] = escapeHTML(unescapedArgs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'jQuery loaded' || type === 'jQuery version change') {
|
||||
return args[0];
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'jQuery - onDOMReady':
|
||||
case 'jQuery - windowOnLoad':
|
||||
return '(function)';
|
||||
|
||||
case 'jQuery - Sizzle call':
|
||||
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
|
||||
|
||||
case 'jQuery - find':
|
||||
if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') {
|
||||
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return '<b>' + args[0] + '</b>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - html':
|
||||
if (args[0] !== undefined) {
|
||||
return 'set content "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - append':
|
||||
return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - appendTo':
|
||||
return 'append ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - prepend':
|
||||
return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - prependTo':
|
||||
return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - before':
|
||||
return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - insertBefore':
|
||||
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - after':
|
||||
return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - insertAfter':
|
||||
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - remove':
|
||||
case 'jQuery - detach':
|
||||
if (args[0]) {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - empty':
|
||||
case 'jQuery - clone':
|
||||
case 'jQuery - unwrap':
|
||||
case 'jQuery - show':
|
||||
case 'jQuery - hide':
|
||||
case 'jQuery - animate':
|
||||
case 'jQuery - fadeIn':
|
||||
case 'jQuery - fadeOut':
|
||||
case 'jQuery - fadeTo':
|
||||
case 'jQuery - fadeToggle':
|
||||
case 'jQuery - slideDown':
|
||||
case 'jQuery - slideUp':
|
||||
case 'jQuery - slideToggle':
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - replaceWith':
|
||||
return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - replaceAll':
|
||||
return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - text':
|
||||
if (args[0] !== undefined) {
|
||||
return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - wrap':
|
||||
case 'jQuery - wrapAll':
|
||||
return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - wrapInner':
|
||||
return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - css':
|
||||
case 'jQuery - attr':
|
||||
case 'jQuery - prop':
|
||||
if (isStringOfObject(args[0])) {
|
||||
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else if (args[1]) {
|
||||
return 'set <b>' + args[0] + '</b> : <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - offset':
|
||||
case 'jQuery - height':
|
||||
case 'jQuery - innerHeight':
|
||||
case 'jQuery - width':
|
||||
case 'jQuery - innerWidth':
|
||||
case 'jQuery - scrollLeft':
|
||||
case 'jQuery - scrollTop':
|
||||
case 'jQuery - position':
|
||||
if (args[0]) {
|
||||
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - outerHeight':
|
||||
case 'jQuery - outerWidth':
|
||||
if (args[0] && args[0] !== 'true') {
|
||||
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else if (args[0] === 'true') {
|
||||
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' (with include margins option)';
|
||||
} else {
|
||||
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - toggle':
|
||||
if (args[0] === 'true') {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
|
||||
} else if (args[0] === 'false') {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
|
||||
} else {
|
||||
return getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - on':
|
||||
case 'jQuery - one':
|
||||
if (isStringOfObject(args[0])) {
|
||||
return '<b>' + args[0].replace(/"\(function\)"/g, '(function)') + '</b>';
|
||||
} else if (args[1] && isPureString(args[1])) {
|
||||
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - off':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
} else {
|
||||
return 'unbind all events';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - live':
|
||||
case 'jQuery - bind':
|
||||
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - die':
|
||||
case 'jQuery - unbind':
|
||||
if (args[0]) {
|
||||
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'unbind all events';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - delegate':
|
||||
return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
|
||||
|
||||
case 'jQuery - undelegate':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'unbind namespace <b>' + args[0] + '</b>';
|
||||
}
|
||||
} else {
|
||||
return 'unbind all events';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - blur':
|
||||
case 'jQuery - change':
|
||||
case 'jQuery - click':
|
||||
case 'jQuery - dblclick':
|
||||
case 'jQuery - focus':
|
||||
case 'jQuery - keydown':
|
||||
case 'jQuery - keypress':
|
||||
case 'jQuery - keyup':
|
||||
case 'jQuery - mousedown':
|
||||
case 'jQuery - mouseenter':
|
||||
case 'jQuery - mouseleave':
|
||||
case 'jQuery - mousemove':
|
||||
case 'jQuery - mouseout':
|
||||
case 'jQuery - mouseover':
|
||||
case 'jQuery - mouseup':
|
||||
case 'jQuery - resize':
|
||||
case 'jQuery - scroll':
|
||||
case 'jQuery - select':
|
||||
case 'jQuery - submit':
|
||||
if (args[0]) {
|
||||
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'triggered on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - error':
|
||||
case 'jQuery - focusin':
|
||||
case 'jQuery - focusout':
|
||||
case 'jQuery - hover':
|
||||
case 'jQuery - load':
|
||||
case 'jQuery - unload':
|
||||
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - removeAttr':
|
||||
case 'jQuery - removeProp':
|
||||
return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - val':
|
||||
if (args[0]) {
|
||||
return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - hasClass':
|
||||
case 'jQuery - addClass':
|
||||
case 'jQuery - removeClass':
|
||||
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - toggleClass':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
} else {
|
||||
return 'magic no-argument toggleClass';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - children':
|
||||
if (args[0]) {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - closest':
|
||||
if (args[1]) {
|
||||
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - next':
|
||||
case 'jQuery - nextAll':
|
||||
if (args[0]) {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - nextUntil':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||
}
|
||||
} else {
|
||||
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - offsetParent':
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - prev':
|
||||
case 'jQuery - prevAll':
|
||||
if (args[0]) {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - prevUntil':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||
}
|
||||
} else {
|
||||
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - parent':
|
||||
case 'jQuery - parents':
|
||||
if (args[0]) {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - parentsUntil':
|
||||
if (args[0]) {
|
||||
if (args[1]) {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||
}
|
||||
} else {
|
||||
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - siblings':
|
||||
if (args[0]) {
|
||||
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||
} else {
|
||||
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHTML(html) {
|
||||
var entityMap = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
"/": '/'
|
||||
};
|
||||
|
||||
return String(html).replace(/[&<>"'\/]/g, function (s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
function joinArgs(args) {
|
||||
var html = '<b>' + args[0] + '</b>';
|
||||
if (args[1]) {
|
||||
html += ', <b>' + args[1] + '</b>';
|
||||
if (args[2]) {
|
||||
html += ', <b>' + args[2] + '</b>';
|
||||
if (args[3]) {
|
||||
html += ', and more...';
|
||||
}
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function isStringOfObject(str) {
|
||||
return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
|
||||
}
|
||||
|
||||
function isPureString(str) {
|
||||
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
|
||||
}
|
||||
|
||||
function getTimelineParamsHTML(node, onASingleLine) {
|
||||
if (isJQuery(node)) {
|
||||
return getJQueryHTML(node, onASingleLine);
|
||||
} else {
|
||||
return getNonJQueryHTML(node, onASingleLine);
|
||||
}
|
||||
}
|
||||
|
||||
function getBacktraceHTML(backtrace) {
|
||||
var html = '';
|
||||
var parsedBacktrace = parseBacktrace(backtrace);
|
||||
|
||||
if (!parsedBacktrace || parsedBacktrace.length === 0) {
|
||||
html += '<div><div>can\'t find any backtrace :/</div></div>';
|
||||
} else {
|
||||
for (var i = 0 ; i < parsedBacktrace.length ; i++) {
|
||||
html += '<div>';
|
||||
html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
|
||||
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + '</div>';
|
||||
if (parsedBacktrace[i].column) {
|
||||
html += '<div>' + parsedBacktrace[i].line + ':' + parsedBacktrace[i].column + '</div>';
|
||||
} else {
|
||||
html += '<div>line ' + parsedBacktrace[i].line + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function parseBacktrace(str) {
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var out = [];
|
||||
var splited = str.split(' / ');
|
||||
|
||||
try {
|
||||
|
||||
splited.forEach(function(trace) {
|
||||
var fnName = null, fileAndLine;
|
||||
|
||||
var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
|
||||
|
||||
if (withFnResult === null) {
|
||||
// Try the PhantomJS 2 format
|
||||
withFnResult = /^([^\s\(]+) \((.+:\d+:\d+)\)$/.exec(trace);
|
||||
}
|
||||
|
||||
if (withFnResult === null) {
|
||||
// Yet another PhantomJS 2 format?
|
||||
withFnResult = /^([^\s\(]+|global code)@(.+:\d+:\d+)$/.exec(trace);
|
||||
}
|
||||
|
||||
if (withFnResult === null) {
|
||||
// Try the PhantomJS 2 ERROR format
|
||||
withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
|
||||
}
|
||||
|
||||
if (withFnResult === null) {
|
||||
fileAndLine = trace;
|
||||
} else {
|
||||
fnName = withFnResult[1];
|
||||
fileAndLine = withFnResult[2];
|
||||
}
|
||||
|
||||
// And now the second part
|
||||
var fileAndLineSplit = /^(.*):(\d+):(\d+)$/.exec(fileAndLine);
|
||||
|
||||
if (fileAndLineSplit === null) {
|
||||
fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
|
||||
}
|
||||
|
||||
var filePath = fileAndLineSplit[1];
|
||||
var line = fileAndLineSplit[2];
|
||||
var column = fileAndLineSplit[3];
|
||||
|
||||
// Filter phantomas code
|
||||
if (filePath.indexOf('phantomjs://') === -1) {
|
||||
out.push({
|
||||
fnName: fnName,
|
||||
filePath: filePath,
|
||||
line: line,
|
||||
column: column
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getTimelineDetailsHTML(node) {
|
||||
var html = '';
|
||||
|
||||
if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
|
||||
if (node.warning || node.error) {
|
||||
html += '<div class="icon-warning"></div>';
|
||||
} else {
|
||||
html += '<div class="icon-question"></div>';
|
||||
}
|
||||
|
||||
html += '<div class="detailsOverlay">';
|
||||
html += '<div class="closeBtn">✖</div>';
|
||||
|
||||
if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
|
||||
html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
|
||||
} else if (node.eventNotDelegated) {
|
||||
html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
|
||||
}
|
||||
|
||||
if (node.data.resultsNumber === 0) {
|
||||
html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
|
||||
} else if (node.data.resultsNumber > 0) {
|
||||
html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
|
||||
}
|
||||
|
||||
if (node.data.backtrace) {
|
||||
html += '<h4>Backtrace</h4>';
|
||||
html += '<div class="table">';
|
||||
html += getBacktraceHTML(node.data.backtrace);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
|
||||
|
||||
var numberWithCommas = $filter('number');
|
||||
|
||||
function getProfilerLineHTML(index, node) {
|
||||
return '<div class="index">' + (index + 1) + '</div>' +
|
||||
'<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
|
||||
'<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
|
||||
'<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
|
||||
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
|
||||
}
|
||||
|
||||
function recursiveChildrenHTML(node) {
|
||||
var html = '';
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach(function(child) {
|
||||
html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function onDetailsClick(row) {
|
||||
// Close if it's alreay open
|
||||
if (row.classList.contains('showDetails')) {
|
||||
closeDetails(row);
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any other open details overlay
|
||||
var openOnes = document.getElementsByClassName('showDetails');
|
||||
if (openOnes.length > 0) {
|
||||
openOnes[0].classList.remove('showDetails');
|
||||
}
|
||||
|
||||
// Make it appear
|
||||
row.classList.add('showDetails');
|
||||
|
||||
// Bind the close button
|
||||
row.querySelector('.closeBtn').addEventListener('click', function() {
|
||||
closeDetails(row);
|
||||
});
|
||||
}
|
||||
|
||||
function closeDetails(row) {
|
||||
row.classList.remove('showDetails');
|
||||
|
||||
// Unbind the close button
|
||||
row.querySelector('.closeBtn').removeEventListener('click', closeDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
index: '=',
|
||||
node: '='
|
||||
},
|
||||
template: '<div></div>',
|
||||
replace: true,
|
||||
link: function(scope, element) {
|
||||
|
||||
if (scope.node.error) {
|
||||
element.addClass('jsError');
|
||||
} else if (scope.node.windowPerformance) {
|
||||
element.addClass('windowPerformance');
|
||||
}
|
||||
|
||||
element.append(getProfilerLineHTML(scope.index, scope.node));
|
||||
element[0].id = 'line_' + scope.index;
|
||||
|
||||
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) {
|
||||
detailsIcon.addEventListener('click', function() {
|
||||
onDetailsClick(this.parentNode.parentNode);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
function shortenUrl(url, maxLength) {
|
||||
if (!maxLength) {
|
||||
maxLength = 110;
|
||||
}
|
||||
|
||||
// Why dividing by 2.1? Because it adds a 5% margin.
|
||||
var leftLength = Math.floor((maxLength - 5) / 2.1);
|
||||
var rightLength = Math.ceil((maxLength - 5) / 2.1);
|
||||
|
||||
return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
|
||||
}
|
||||
|
||||
offendersDirectives.filter('shortenUrl', function() {
|
||||
return shortenUrl;
|
||||
});
|
||||
|
||||
function getUrlLink(url, maxLength) {
|
||||
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
|
||||
}
|
||||
|
||||
offendersDirectives.directive('urlLink', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
url: '=',
|
||||
maxLength: '='
|
||||
},
|
||||
template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
|
||||
replace: true
|
||||
};
|
||||
});
|
||||
|
||||
offendersDirectives.filter('encodeURIComponent', function() {
|
||||
return window.encodeURIComponent;
|
||||
});
|
||||
|
||||
offendersDirectives.directive('fileAndLine', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
file: '=',
|
||||
line: '=',
|
||||
column: '='
|
||||
},
|
||||
template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file"><inline CSS></span><span ng-if="line !== null && column !== null"> @ {{line}}:{{column}}</span></span>',
|
||||
replace: true
|
||||
};
|
||||
});
|
||||
|
||||
offendersDirectives.directive('fileAndLineButton', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
file: '=',
|
||||
line: '=',
|
||||
column: '='
|
||||
},
|
||||
template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
|
||||
replace: true
|
||||
};
|
||||
});
|
||||
|
||||
offendersDirectives.filter('bytes', function() {
|
||||
return function(bytes) {
|
||||
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
var kilo = bytes / 1024;
|
||||
|
||||
if (kilo < 1) {
|
||||
return bytes + ' bytes';
|
||||
}
|
||||
|
||||
if (kilo < 100) {
|
||||
return kilo.toFixed(1) + ' KB';
|
||||
}
|
||||
|
||||
if (kilo < 1024) {
|
||||
return kilo.toFixed(0) + ' KB';
|
||||
}
|
||||
|
||||
var mega = kilo / 1024;
|
||||
|
||||
if (mega < 10) {
|
||||
return mega.toFixed(2) + ' MB';
|
||||
}
|
||||
|
||||
return mega.toFixed(1) + ' MB';
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -1,7 +0,0 @@
|
||||
var resultsFactory = angular.module('resultsFactory', ['ngResource']);
|
||||
|
||||
resultsFactory.factory('Results', ['$resource', function($resource) {
|
||||
return $resource('/api/results/:runId', {
|
||||
|
||||
});
|
||||
}]);
|
||||
@@ -1,7 +0,0 @@
|
||||
var runsFactory = angular.module('runsFactory', ['ngResource']);
|
||||
|
||||
runsFactory.factory('Runs', ['$resource', function($resource) {
|
||||
return $resource('/api/runs/:runId', {
|
||||
|
||||
});
|
||||
}]);
|
||||
@@ -1,61 +0,0 @@
|
||||
var apiService = angular.module('apiService', []);
|
||||
|
||||
apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
|
||||
|
||||
return {
|
||||
|
||||
launchTest: function(url, settings) {
|
||||
var runObject = {
|
||||
url: url,
|
||||
waitForResponse: false,
|
||||
screenshot: true,
|
||||
device: settings.device,
|
||||
waitForSelector: settings.waitForSelector,
|
||||
cookie: settings.cookie,
|
||||
authUser: settings.authUser,
|
||||
authPass: settings.authPass,
|
||||
blockDomain: settings.blockDomain,
|
||||
allowedDomains: settings.allowedDomains,
|
||||
noExternals: settings.noExternals
|
||||
};
|
||||
|
||||
|
||||
if (settings.domainsBlackOrWhite === 'black') {
|
||||
runObject.blockDomain = this.parseDomains(settings.domains);
|
||||
} else if (settings.domainsBlackOrWhite === 'white') {
|
||||
var allowedDomains = this.parseDomains(settings.domains);
|
||||
if (allowedDomains.length > 0) {
|
||||
runObject.allowDomain = allowedDomains;
|
||||
} else {
|
||||
runObject.noExternals = true;
|
||||
}
|
||||
}
|
||||
|
||||
Runs.save(runObject, function(data) {
|
||||
$location.path('/queue/' + data.runId);
|
||||
}, function(response) {
|
||||
if (response.status === 429) {
|
||||
alert('Too many requests, you reached the max number of requests allowed in 24h');
|
||||
} else {
|
||||
alert('An error occured...');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
relaunchTest: function(result) {
|
||||
this.launchTest(result.params.url, result.params.options);
|
||||
},
|
||||
|
||||
parseDomains: function(textareaContent) {
|
||||
var lines = textareaContent.split('\n');
|
||||
|
||||
function removeEmptyLines (line) {
|
||||
return line.trim() !== '';
|
||||
}
|
||||
|
||||
// Remove empty lines
|
||||
return lines.filter(removeEmptyLines).join(',');
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -1,34 +0,0 @@
|
||||
var menuService = angular.module('menuService', []);
|
||||
|
||||
menuService.factory('Menu', ['$location', function($location) {
|
||||
|
||||
var currentPage, currentRunId;
|
||||
|
||||
return {
|
||||
getCurrentPage: function() {
|
||||
return currentPage;
|
||||
},
|
||||
setCurrentPage: function(page, runId) {
|
||||
currentPage = page;
|
||||
currentRunId = runId;
|
||||
|
||||
return this;
|
||||
},
|
||||
changePage: function(page) {
|
||||
switch (page) {
|
||||
case 'index':
|
||||
$location.path('/');
|
||||
break;
|
||||
case 'dashboard':
|
||||
$location.path('/result/' + currentRunId);
|
||||
break;
|
||||
case 'timeline':
|
||||
$location.path('/result/' + currentRunId + '/timeline');
|
||||
break;
|
||||
default:
|
||||
console.err('Undefined Menu.changePage() destination');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -1,24 +0,0 @@
|
||||
var settingsService = angular.module('settingsService', []);
|
||||
|
||||
settingsService.factory('Settings', ['localStorageService', function(localStorageService) {
|
||||
|
||||
return {
|
||||
|
||||
getMergedSettings: function() {
|
||||
var defaultSettings = {
|
||||
device: 'desktop',
|
||||
showAdvanced: false
|
||||
};
|
||||
|
||||
var savedValues = localStorageService.get('settings');
|
||||
|
||||
return angular.extend(defaultSettings, savedValues);
|
||||
},
|
||||
|
||||
saveSettings: function(settings) {
|
||||
localStorageService.set('settings', settings);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -1,12 +0,0 @@
|
||||
.about {
|
||||
margin: 3em auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.about p {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
.about a {
|
||||
color: #FFF;
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
.testedUrl {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.summary {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary .globalScore {
|
||||
display: table;
|
||||
width: 60%;
|
||||
margin: 3em auto;
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
width: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.on100 {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0 1em;
|
||||
}
|
||||
|
||||
.screenshotWrapper:hover {
|
||||
opacity: 0.75;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
top: 0.7em;
|
||||
left: 1.55em;
|
||||
font-size: 3em;
|
||||
color: #FFF;
|
||||
background: #000;
|
||||
border-radius: 0.2em;
|
||||
text-align: center;
|
||||
content: "+";
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.phone:hover:after {
|
||||
top: 1.7em;
|
||||
left: 0.64em;
|
||||
}
|
||||
|
||||
.screenshotWrapper.tablet:hover:after {
|
||||
top: 1.5em;
|
||||
left: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
.A, .B, .C, .D, .E, .F, .NA {
|
||||
.summary .notations &.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 & {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-size: 1em;
|
||||
color: transparent;
|
||||
margin: 0 auto;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.summary .notations .criteria .table {
|
||||
width: 100%;
|
||||
> a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
> a:hover > div {
|
||||
background: #EBD8E2;
|
||||
cursor: pointer;
|
||||
&.info {
|
||||
background: #FFF;
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
.tweetButton, .linkedinButton {
|
||||
color: #413;
|
||||
background: #F2F2F2;
|
||||
margin-right: 0;
|
||||
&:hover {
|
||||
color: #F2F2F2;
|
||||
background: #e74c3c;
|
||||
}
|
||||
}
|
||||
input {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
.promess {
|
||||
padding: 0em 2em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.price {
|
||||
padding: 0em 2em 3em;
|
||||
margin-top: 0em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.url {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.launchBtn {
|
||||
background: #ffa319;
|
||||
color: #fff;
|
||||
&:focus {
|
||||
background: #e74c3c;
|
||||
}
|
||||
&.disabled {
|
||||
background: #deaca6;
|
||||
&:focus {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.settings {
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
|
||||
input, select {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], textarea {
|
||||
width: 100%;
|
||||
min-width: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.device {
|
||||
margin-top: 3em;
|
||||
.item {
|
||||
display: inline-block;
|
||||
margin: 1em 0.75em;
|
||||
width: 5.5em;
|
||||
height: 5.5em;
|
||||
color: #FFF;
|
||||
border: 1px solid #FFF;
|
||||
padding: 1px;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
|
||||
&.active {
|
||||
color: #ffa319;
|
||||
border: 2px solid #ffa319;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin: 0.2em 0 0.1em;
|
||||
font-size: 3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settingsTooltip {
|
||||
position: relative;
|
||||
span {
|
||||
font-size: 0.8em;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding: 0.5em;
|
||||
width: 25em;
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
font-size: 0.8em;
|
||||
border-radius: 1em;
|
||||
border: 2px solid #ffa319;
|
||||
white-space: normal;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover div {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.showAdvanced {
|
||||
display: inline-block;
|
||||
margin-top: 2em;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
}
|
||||
|
||||
.currentSettings {
|
||||
font-size: 0.9em;
|
||||
|
||||
span {
|
||||
color: #ffa319;
|
||||
&:after {
|
||||
color: #FFF;
|
||||
content: ",";
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.advanced {
|
||||
margin: 1em 0 0;
|
||||
display: table;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-spacing: 0.75em;
|
||||
|
||||
> div {
|
||||
display: table-row;
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
width: 75%;
|
||||
|
||||
&.label {
|
||||
width: 25%;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subTable {
|
||||
display: table;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
> div {
|
||||
display: table-row;
|
||||
> div {
|
||||
display: table-cell;
|
||||
padding: 0 0 0.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
display: table;
|
||||
width: 50%;
|
||||
margin: 6em auto 0;
|
||||
font-size: 0.9em;
|
||||
color: #413;
|
||||
|
||||
> 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;
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
@import "icons.less";
|
||||
|
||||
html {
|
||||
margin: 100px 50px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 1280px;
|
||||
background: #9c4274;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body, input[type=submit], input[type=text], input[type=url], input[type=number], button {
|
||||
font-family: 'Century Gothic', helvetica, arial, sans-serif;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
color: #ffa319;
|
||||
}
|
||||
|
||||
.resultsMenu {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.resultsMenu .menuItem {
|
||||
display: inline-block;
|
||||
margin: 1em;
|
||||
width: 8em;
|
||||
height: 7em;
|
||||
color: #fff;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 0.5em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
&.back, &.restart {
|
||||
color: #413;
|
||||
border-color: #413;
|
||||
}
|
||||
}
|
||||
.resultsMenu .menuItem div {
|
||||
padding-top: 0.5em;
|
||||
font-size: 3em;
|
||||
}
|
||||
.resultsMenu .active, .resultsMenu .menuItem.active:hover {
|
||||
color: #ffa319;
|
||||
border-color: #ffa319;
|
||||
}
|
||||
.resultsMenu .menuItem:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
.resultsMenu span {
|
||||
position: relative;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
/* Grade colors */
|
||||
.A {
|
||||
/* green */
|
||||
background: #0C4;
|
||||
}
|
||||
.B {
|
||||
/* green */
|
||||
background: #CD0;
|
||||
}
|
||||
.C {
|
||||
/* yellow */
|
||||
background: #FD2;
|
||||
}
|
||||
.D {
|
||||
/* orange */
|
||||
background: #FA2;
|
||||
}
|
||||
.E {
|
||||
/* red */
|
||||
background: #F60;
|
||||
}
|
||||
.F {
|
||||
/* red */
|
||||
background: #F22;
|
||||
}
|
||||
.NA {
|
||||
/* Non applicable */
|
||||
background: #CCC;
|
||||
}
|
||||
|
||||
.board {
|
||||
margin-top: 2em;
|
||||
padding: 1em;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border-radius: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.backToDashboard {
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
font-size: 0.9em;
|
||||
display: block;
|
||||
margin-top: 4em;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
a.linkButton {
|
||||
font-size: 1em;
|
||||
padding: 0.3em 0.5em;
|
||||
margin: 0.5em;
|
||||
line-height: 2em;
|
||||
border: 0 solid;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0.1em 0.2em 0 0 #5e2846;
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
.screenshotWrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #000;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotImage {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screenshotError {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.desktop {
|
||||
border: 0.2em solid #AAA;
|
||||
padding: 0.5em;
|
||||
border-top-left-radius: 0.4em;
|
||||
border-top-right-radius: 0.4em;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
width: 15em;
|
||||
height: 0.6em;
|
||||
bottom: -0.75em;
|
||||
left: -1em;
|
||||
background: #CCC;
|
||||
border-bottom-left-radius: 0.2em;
|
||||
border-bottom-right-radius: 0.2em;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
width: 0.4em;
|
||||
height: 0.2em;
|
||||
bottom: -0.55em;
|
||||
left: 12.5em;
|
||||
background: lime;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
> div {
|
||||
width: 12em;
|
||||
height: 6.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.phone {
|
||||
border: 0.07em solid #CCC;
|
||||
padding: 1em 0.3em 1.5em;
|
||||
border-radius: 0.6em;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
bottom: 0.3em;
|
||||
left: 3.3em;
|
||||
border: 0.07em solid #CCC;
|
||||
border-radius: 0.5em;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
width: 1em;
|
||||
height: 0.1em;
|
||||
bottom: 13.9em;
|
||||
left: 3.2em;
|
||||
background: #555;
|
||||
border-radius: 0.05em;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
> div {
|
||||
width: 6.75em;
|
||||
height: 12em;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.tablet {
|
||||
border: 0.07em solid #CCC;
|
||||
padding: 0.8em 0.5em 0.9em;
|
||||
border-radius: 0.6em;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
bottom: 0.15em;
|
||||
left: 4.35em;
|
||||
border: 0.07em solid #CCC;
|
||||
border-radius: 0.4em;
|
||||
content: " ";
|
||||
}
|
||||
|
||||
> div {
|
||||
width: 8.25em;
|
||||
height: 11em;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 3em;
|
||||
color: #413;
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
.version {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.github {
|
||||
margin: 1em 0 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.status {
|
||||
margin-top: 2em;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.statusSubMessage {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 6em;
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
.rule.board {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rule .ruleTable {
|
||||
display: table;
|
||||
border-spacing: 1em;
|
||||
width: 90%;
|
||||
margin: 2em auto;
|
||||
background: #f2f2f2;
|
||||
border: 1px dashed #666;
|
||||
border-radius: 0.5em;
|
||||
> div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.left {
|
||||
width: 33%;
|
||||
font-weight: bold;
|
||||
}
|
||||
.right {
|
||||
width: 67%;
|
||||
}
|
||||
}
|
||||
|
||||
.rule .score {
|
||||
font-size: 2.5em;
|
||||
line-height: 2em;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
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: 1.5em auto;
|
||||
p {
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.rule .message ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
.rule .message li:before {
|
||||
content:'\25e6';
|
||||
margin-right: 0.3em;
|
||||
font-size: 1.2em;
|
||||
position: relative;
|
||||
top: 0.1em;
|
||||
}
|
||||
|
||||
.rule .warning {
|
||||
width: 90%;
|
||||
margin: -1em auto 2em;
|
||||
background: #FEE;
|
||||
border: 1px dashed #e74c3c;
|
||||
color: #e74c3c;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.rule .offendersTable {
|
||||
display: table;
|
||||
border-spacing: 0 0.25em;
|
||||
margin: 0 auto;
|
||||
min-width: 10%;
|
||||
max-width: 90%;
|
||||
> div {
|
||||
display: table-row;
|
||||
> div {
|
||||
display: table-cell;
|
||||
background: #f2f2f2;
|
||||
padding: 0 1em;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
&:hover {
|
||||
background: #EBD8E2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rule .notFound {
|
||||
font-size: 1em;
|
||||
h2 {
|
||||
font-size: 3em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.rule .startTime {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.offendersTable, .value {
|
||||
.offenderButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #efe;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&.opens {
|
||||
padding-right: 0.75em;
|
||||
|
||||
&:after {
|
||||
position: relative;
|
||||
left: 0.5em;
|
||||
content: '\25BC';
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: 100%;
|
||||
background: inherit;
|
||||
border-bottom-left-radius: 0.4em;
|
||||
border-bottom-right-radius: 0.4em;
|
||||
border-top: 1px solid #999;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.domTree {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
> div {
|
||||
margin: 0.5em;
|
||||
|
||||
div {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.backtrace, .cssFileAndLine {
|
||||
white-space: nowrap;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
&.opens:hover {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: #ffe0cc;
|
||||
z-index: 2;
|
||||
|
||||
> div {
|
||||
display: block;
|
||||
background: #ffe0cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smallerOffenders {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.offendersHtml {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.domTree div {
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
|
||||
span:only-child {
|
||||
font-weight: bold;
|
||||
span {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checker {
|
||||
/* Checkerboard background */
|
||||
background-color: #ddd;
|
||||
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
|
||||
background-size:1em 1em;
|
||||
background-position:0 0, 0.5em 0.5em;
|
||||
}
|
||||
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
|
||||
div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
background: #FFF;
|
||||
padding: 0.5em;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
white-space: nowrap;
|
||||
z-index: 3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
left: -0.2em;
|
||||
top: -0.2em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.similarColors {
|
||||
margin: 1em;
|
||||
width: 20em;
|
||||
height: 6em;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
height: 3.5em;
|
||||
padding-top: 2.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.totalWeightPie {
|
||||
max-width: 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;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.screenshot.board {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screenshot .screenshotWrapper {
|
||||
font-size: 2.08333333333333em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
/* Timeline colors, related to Window Performances */
|
||||
@domCreationColor: #FF6600;
|
||||
@domCreationBg: #FFE0CC;
|
||||
@domContentLoadedColor: #A7E846;
|
||||
@domContentLoadedBg: #E0FFD1;
|
||||
@domContentLoadedEndColor: #7ECCCC;
|
||||
@domContentLoadedEndBg: #D8F0F0;
|
||||
@domCompleteColor: #C2A3FF;
|
||||
@domCompleteBg: #EDE3FF;
|
||||
@domInteractiveColor: #FFE433;
|
||||
@domInteractiveBg: #FFFCCC;
|
||||
|
||||
.execution {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selectScript {
|
||||
padding-bottom: 2em;
|
||||
font-size: 0.9em;
|
||||
|
||||
select {
|
||||
max-width: 30em;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
font-size: 0.8em;
|
||||
|
||||
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%;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.timeline div.interval:hover {
|
||||
background: #9C4274;
|
||||
}
|
||||
.timeline .interval:hover .color {
|
||||
background: #F04DA7;
|
||||
}
|
||||
.timeline .domComplete.interval {
|
||||
background: @domCompleteBg;
|
||||
}
|
||||
.timeline .domComplete .color {
|
||||
background: @domCompleteColor;
|
||||
}
|
||||
.timeline .domContentLoadedEnd.interval {
|
||||
background: @domContentLoadedEndBg;
|
||||
}
|
||||
.timeline .domContentLoadedEnd .color {
|
||||
background: @domContentLoadedEndColor;
|
||||
}
|
||||
.timeline .domContentLoaded.interval {
|
||||
background: @domContentLoadedBg;
|
||||
}
|
||||
.timeline .domContentLoaded .color {
|
||||
background: @domContentLoadedColor;
|
||||
}
|
||||
.timeline .domInteractive.interval {
|
||||
background: @domInteractiveBg;
|
||||
}
|
||||
.timeline .domInteractive .color {
|
||||
background: @domInteractiveColor;
|
||||
}
|
||||
.timeline .domCreation.interval {
|
||||
background: @domCreationBg;
|
||||
}
|
||||
.timeline .domCreation .color {
|
||||
background: @domCreationColor;
|
||||
}
|
||||
.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,
|
||||
.table > a {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.table > .headers > div {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.table > div > div,
|
||||
.table > a > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
.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;
|
||||
|
||||
> 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;
|
||||
|
||||
> .child {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "↳";
|
||||
}
|
||||
|
||||
.childArgs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
background: #EBD8E2;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.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: @domCompleteBg;
|
||||
}
|
||||
.table > div > .startTime.domContentLoadedEnd {
|
||||
background: @domContentLoadedEndBg;
|
||||
}
|
||||
.table > div > .startTime.domContentLoaded {
|
||||
background: @domContentLoadedBg;
|
||||
}
|
||||
.table > div > .startTime.domInteractive {
|
||||
background: @domInteractiveBg;
|
||||
}
|
||||
.table > div > .startTime.domCreation {
|
||||
background: @domCreationBg;
|
||||
}
|
||||
.execution .icon-warning {
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.queryWithoutResultsFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Yellow Lab Tools</title>
|
||||
<base href="/">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<meta property="og:image" content="/img/logo-large.png" />
|
||||
<meta name="description" content="Free online web performance tool. Audit your webpage for performance and front-end quality issues. And it's open-source!" />
|
||||
|
||||
<!-- build:css /css/styles.css-->
|
||||
<link rel="stylesheet" type="text/css" href="/css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/index.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/dashboard.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/queue.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/rule.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/screenshot.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/timeline.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/about.css">
|
||||
<link rel="stylesheet" type="text/css" href="/node_modules/angular-chart.js/dist/angular-chart.css">
|
||||
<!-- endbuild -->
|
||||
|
||||
<link rel="preconnect" href="//www.google-analytics.com">
|
||||
<link rel="dns-prefetch" href="//www.google-analytics.com">
|
||||
<link rel="preconnect" href="//ghbtns.com">
|
||||
<link rel="dns-prefetch" href="//ghbtns.com">
|
||||
<link rel="preconnect" href="https://api.github.com">
|
||||
<link rel="dns-prefetch" href="https://api.github.com">
|
||||
|
||||
<head>
|
||||
|
||||
<body ng-app="YellowLabTools">
|
||||
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
|
||||
<div id="body" ng-view autoscroll="true"></div>
|
||||
<div class="footer">
|
||||
<span class="version">@@version</span>
|
||||
<br><a href="/about">More about Yellow Lab Tools</a><br>
|
||||
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
|
||||
</div>
|
||||
|
||||
<!-- build:js /js/all.js -->
|
||||
<script src="/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>
|
||||
<script src="/js/controllers/queueCtrl.js"></script>
|
||||
<script src="/js/controllers/ruleCtrl.js"></script>
|
||||
<script src="/js/controllers/screenshotCtrl.js"></script>
|
||||
<script src="/js/controllers/timelineCtrl.js"></script>
|
||||
<script src="/js/models/resultsFactory.js"></script>
|
||||
<script src="/js/models/runsFactory.js"></script>
|
||||
<script src="/js/services/apiService.js"></script>
|
||||
<script src="/js/services/menuService.js"></script>
|
||||
<script src="/js/services/settingsService.js"></script>
|
||||
|
||||
<script src="/js/directives/gradeDirective.js"></script>
|
||||
<script src="/js/directives/offendersDirectives.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +0,0 @@
|
||||
<div class="about">
|
||||
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>. It lets you test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
|
||||
|
||||
<p>This is done by loading the webpage via PhantomJS and collecting various metrics and statistics with the help of <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>. These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.</p>
|
||||
|
||||
<p>By the way, <b>it's free</b> because we are geeks, not businessmen. All we want is <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">a <span>★</span> on GitHub</a>. It will boost our motivation to add more awesome features!!!</p>
|
||||
|
||||
<p><br><a href="/">Back to index</a></p>
|
||||
</div>
|
||||
@@ -1,79 +0,0 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="summary board">
|
||||
|
||||
<div ng-if="result.blockedRequests">
|
||||
<b><ng-pluralize count="result.blockedRequests.length" when="{'0': 'No blocked request', 'one': '1 blocked request', 'other': '{} blocked requests'}"></ng-pluralize>:</b>
|
||||
<div ng-repeat="request in result.blockedRequests">
|
||||
{{request}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
|
||||
<div>
|
||||
<h2>Global score</h2>
|
||||
<div class="globalScoreDisplay">
|
||||
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
|
||||
<div class="on100">{{globalScore}}/100</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/result/{{result.runId}}/screenshot">
|
||||
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
|
||||
<div>
|
||||
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 ng-if="!error && !fromSocialShare">Score details</h2>
|
||||
<div ng-if="!error && !fromSocialShare" class="notations">
|
||||
<div ng-repeat="categoryKey in categoriesOrder" ng-init="category = result.scoreProfiles.generic.categories[categoryKey]">
|
||||
<grade score="category.categoryScore" class="categoryScore"></grade>
|
||||
<div class="category">{{category.label}}</div>
|
||||
<div class="criteria">
|
||||
<div class="table" title="Click to see details">
|
||||
<a ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
|
||||
ng-class="{'warning': rule.abnormal}" href="/result/{{runId}}/rule/{{ruleName}}">
|
||||
<div class="grade">
|
||||
<grade score="rule.score"></grade>
|
||||
</div>
|
||||
<div class="label">{{rule.policy.label}}</div>
|
||||
<div class="result">
|
||||
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
|
||||
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!error && !fromSocialShare" class="apiTip">
|
||||
<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">
|
||||
<form ng-submit="sendTweet()">
|
||||
<input type="text" class="tweetText" ng-model="tweetText">
|
||||
<input type="submit" class="tweetButton" value="Tweet this!" ng-click="shareOnTwitter(tweetText)">
|
||||
<input type="submit" class="linkedinButton" value="LinkedIn this!" ng-click="shareOnLinkedin(tweetText)">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="fromShare" ng-if="!error && fromSocialShare">
|
||||
<p>Yellow Lab Tools is a free online tool that analyzes performance and front-end quality.</p>
|
||||
<a class="linkButton" href="" ng-click="seeFullReport()">See the full report for this page</a>
|
||||
<a class="linkButton" href="/">Test another webpage</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="error">
|
||||
<h2>Run failed / Run not found</h2>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,100 +0,0 @@
|
||||
<h2 class="promess">Online test to help speeding up <b>heavy</b> web pages</h2>
|
||||
<p class="price">Free and open source!</p>
|
||||
|
||||
<form ng-submit="launchTest()" >
|
||||
<input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
|
||||
<input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
|
||||
<div class="settings">
|
||||
<div class="device">
|
||||
<div>Choose the simulated device:</div>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</a>
|
||||
</div>
|
||||
[ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
|
||||
<span ng-if="!settings.showAdvanced">Advanced settings ✚</span>
|
||||
<span ng-if="settings.showAdvanced">Hide advanced settings ✖</span>
|
||||
</a> ]
|
||||
<span class="currentSettings" ng-if="!settings.showAdvanced && (settings.waitForSelector || settings.cookie || settings.authUser || settings.authPass)">
|
||||
Currently set:
|
||||
<span ng-if="settings.waitForSelector">wait for selector</span>
|
||||
<span ng-if="settings.cookie">cookie</span>
|
||||
<span ng-if="settings.authUser || settings.authPass">authentication</span>
|
||||
</span>
|
||||
<div class="advanced" ng-show="settings.showAdvanced">
|
||||
<!--<div>
|
||||
<div class="label">
|
||||
Wait selector
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Wait for a CSS selector</b><br><br>Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.<br><br>Example: "body.loaded"</div>
|
||||
</span>
|
||||
</div>
|
||||
<div><input type="text" name="waitForSelector" ng-model="settings.waitForSelector" /></div>
|
||||
</div>-->
|
||||
<div>
|
||||
<div class="label">
|
||||
Cookie
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Cookie</b><br><br>Adds a cookie on the main domain.<br><br>Example: "bar=foo;domain=url"</div>
|
||||
</span>
|
||||
</div>
|
||||
<div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">
|
||||
Authent
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Basic HTTP authentication</b><br><br>Enter your credentials here if you need to bypass a basic authentication.<br><br><i>PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.</i></div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="subTable">
|
||||
<div>
|
||||
<div>username</div>
|
||||
<div><input type="text" class="authField" name="authUser" ng-model="settings.authUser" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<div><span>password</div>
|
||||
<div><input type="password" class="authField" name="authPass" ng-model="settings.authPass" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">
|
||||
Block domains
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
|
||||
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
|
||||
</div>
|
||||
<textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="features">
|
||||
<div>
|
||||
<h3>Front-end quality</h3>
|
||||
<p>Audits quality problems in the HTML, CSS and JS</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>WebPerf</h3>
|
||||
<p>Checks if performance good practices are applied</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>JS Profiling</h3>
|
||||
<p>Untangles the JavaScript spaghetti code</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,36 +0,0 @@
|
||||
<p ng-if="url">Tested url: <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
|
||||
|
||||
<div ng-if="!notFound && !connectionLost">
|
||||
<div ng-if="status.statusCode == 'failed'">
|
||||
<div class="status">Test failed</div>
|
||||
<p class="statusSubMessage">{{status.error}}</p>
|
||||
|
||||
<a class="linkButton" href="https://github.com/gmetais/YellowLabTools/issues" target="_blank">Report the issue on GitHub</a>
|
||||
<a class="linkButton" href="/">New test</a>
|
||||
</div>
|
||||
<div ng-if="status.statusCode == 'awaiting'">
|
||||
<div class="status">
|
||||
<ng-pluralize count="status.position" when="{'one': 'Waiting behind 1 other test', 'other': 'Waiting behind {} other tests'}">
|
||||
</ng-pluralize>
|
||||
</div>
|
||||
<p class="statusSubMessage">(auto-refresh activated)</p>
|
||||
</div>
|
||||
<div ng-if="status.statusCode == 'running'">
|
||||
<div class="status">Test is running...</div>
|
||||
<p class="statusSubMessage">(auto-refresh activated)</p>
|
||||
</div>
|
||||
<div ng-if="status.statusCode == 'complete'">
|
||||
<div class="status">Test complete</div>
|
||||
<p class="statusSubMessage">Opening results...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="notFound == true">
|
||||
<div class="status">Error 404 (test not found)</div>
|
||||
<p class="statusSubMessage">The server probably just rebooted. We are very sorry about that, please try to launch the test again.</p>
|
||||
|
||||
<a class="linkButton" href="/">New test</a>
|
||||
</div>
|
||||
<div ng-if="connectionLost == true">
|
||||
<div class="status">Connection lost with server</div>
|
||||
<p class="statusSubMessage">Check your wifi cable, or maybe YellowLab.tools is rebooting.</p>
|
||||
</div>
|
||||
@@ -1,8 +0,0 @@
|
||||
<div>Tested url: <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
|
||||
|
||||
<div class="resultsMenu">
|
||||
<a class="menuItem back" href="/"><div class="icon-arrow-left3"></div><span>New test<span></a>
|
||||
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
|
||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-list"></div><span>Dashboard</span></div>
|
||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
|
||||
</div>
|
||||
@@ -1,424 +0,0 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="rule board">
|
||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
||||
|
||||
<div ng-if="rule" class="ruleTable">
|
||||
<div class="left">
|
||||
<h2>{{rule.policy.label}}</h2>
|
||||
<grade score="rule.score" class="score"></grade>
|
||||
<div>{{rule.score}}/100</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>
|
||||
Value:
|
||||
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
|
||||
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
|
||||
</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>
|
||||
<div ng-if="rule.abnormal" class="warning">
|
||||
<h3>Warning</h3>
|
||||
<p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
|
||||
</div>
|
||||
<div class="offenders" ng-if="rule.policy.hasOffenders">
|
||||
<h3 ng-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
|
||||
|
||||
<div ng-if="rule.offendersObj.list" class="offendersTable">
|
||||
<div ng-repeat="offender in rule.offendersObj.list track by $index">
|
||||
<div ng-if="offender.parseError">
|
||||
{{offender.parseError}}
|
||||
</div>
|
||||
<div ng-if="!offender.parseError">
|
||||
|
||||
<div ng-if="policyName === 'DOMidDuplicated'">
|
||||
<b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMqueriesAvoidable'">
|
||||
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}: <b>{{offender.count}} queries</b>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'eventsScrollBound'">
|
||||
<span ng-if="offender.target == 'window'">Scroll event bound on <b>window</b></span>
|
||||
<span ng-if="offender.target == '#document'">Scroll event bound on <b>document</b></span>
|
||||
<span ng-if="offender.target == 'window.onscroll'"><b>window.onscroll</b> function declared</span>
|
||||
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
|
||||
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
|
||||
backtrace
|
||||
<div class="backtrace">
|
||||
<div ng-repeat="obj in offender.backtrace track by $index">
|
||||
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
|
||||
<url-link url="obj.file" max-length="60"></url-link>
|
||||
line {{obj.line}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jsErrors'">
|
||||
<b>{{offender.error}}</b>
|
||||
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
|
||||
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
|
||||
backtrace
|
||||
<div class="backtrace">
|
||||
<div ng-repeat="obj in offender.backtrace track by $index">
|
||||
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
|
||||
<url-link url="obj.file" max-length="60"></url-link>
|
||||
line {{obj.line}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryFunctionsUsed'">
|
||||
function <b>{{offender.functionName}}</b> used {{offender.count}} times
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'documentWriteCalls'">
|
||||
<b>{{offender.writeFn}}</b>
|
||||
<span ng-if="offender.from">
|
||||
called from
|
||||
<span ng-if="offender.from.functionName">{{offender.from.functionName}}()</span>
|
||||
<url-link url="offender.from.file" max-length="50"></url-link>
|
||||
line {{offender.from.line}}
|
||||
</span>
|
||||
<span ng-if="!offender.from">
|
||||
called from (no backtrace available)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssRules'">
|
||||
<span ng-if="offender.file === 'inline CSS'">inline CSS</span>
|
||||
<span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
|
||||
: <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'similarColors'">
|
||||
<div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssParsingErrors'">
|
||||
<b>{{offender.error}}</b>
|
||||
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
|
||||
<span ng-if="offender.file">(<a href="http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&uri={{offender.file | encodeURIComponent}}" target="_blank">Check on the W3C validator</a>)</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssImports'">
|
||||
{{offender.css}}
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssDuplicatedSelectors'">
|
||||
{{offender.rule}} (<b>x{{offender.occurrences}}</b>)
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
|
||||
<b>{{offender.property}} {{offender.message}}</b>
|
||||
<div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
|
||||
<span ng-if="!offender.showMore">show</span>
|
||||
<span ng-if="offender.showMore">hide</span>
|
||||
<ng-pluralize count="offender.rules.length" when="{'one':'1 offender','other':'{} offenders'}"></ng-pluralize>
|
||||
</div>
|
||||
<div ng-if="offender.showMore" class="smallerOffenders">
|
||||
<div ng-repeat="cssRule in offender.rules">
|
||||
{{cssRule.rule}} {{'{' + offender.property}}: {{cssRule.value + '}' }}
|
||||
<file-and-line-button file="cssRule.file" line="cssRule.line" column="cssRule.column"></file-and-line-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === '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 === 'emptyRequests' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
|
||||
<url-link url="offender" max-length="100"></url-link>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cachingTooShort'">
|
||||
<url-link url="offender.file" max-length="100"></url-link>
|
||||
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'domains'">
|
||||
<b>{{offender.domain}}</b>
|
||||
(<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded' || policyName === 'synchronousXHR'">
|
||||
{{offender}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="(file, fileDetails) in rule.offendersObj.byFile track by $index">
|
||||
<h3>
|
||||
<ng-pluralize count="fileDetails.count" when="{'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize>
|
||||
in
|
||||
<url-link ng-if="file !== 'Inline CSS'" url="file" max-length="80"></url-link>
|
||||
<span ng-if="file === 'Inline CSS'">inline CSS</span>
|
||||
</h3>
|
||||
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="offender in fileDetails.offenders track by $index">
|
||||
<div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
|
||||
<span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
|
||||
<b ng-if="!offender.bolded">{{offender.css}}</b>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssMobileFirst'">
|
||||
<b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssDuplicatedProperties'">
|
||||
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssEmptyRules'">
|
||||
<b>{{offender.css}} { }</b>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssExpressions'">
|
||||
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssImportants'">
|
||||
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssOldIEFixes'">
|
||||
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
|
||||
<span ng-bind-html="offender.bolded"></span>
|
||||
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!rule.offendersObj.list && !rule.offendersObj.byFile" class="offendersHtml">
|
||||
|
||||
<div ng-if="policyName === 'DOMelementMaxDepth'">
|
||||
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
|
||||
<p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
|
||||
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
<div>Params</div>
|
||||
<div><!-- details --></div>
|
||||
</div>
|
||||
<profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
|
||||
<p>This is the colors palette, sized by total occurrences:</p>
|
||||
<div class="colorPalette checker">
|
||||
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'totalWeight'">
|
||||
<h3>Weight by MIME type</h3>
|
||||
<div class="totalWeightPie">
|
||||
<canvas class="chart chart-doughnut" data="weightData" labels="weightLabels" options="weightOptions" colours="weightColours" legend="true"></canvas>
|
||||
</div>
|
||||
<div ng-repeat="type in weightLabels">
|
||||
<h3>{{rule.offendersObj.list.byType[type].totalWeight | bytes}} of {{type}}</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="request in rule.offendersObj.list.byType[type].requests | orderBy:'-weight'" ng-if="request.weight > 0">
|
||||
<div><url-link url="request.url" max-length="60"></url-link></div>
|
||||
<div ng-class="{hugeFile: request.weight > 102400}">{{request.weight | bytes}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'imageOptimization'">
|
||||
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 image', 'other': '{} images'}"></ng-pluralize></h3>
|
||||
<div class="imageOffenders">
|
||||
<div ng-repeat="image in rule.offendersObj.list.images | orderBy:'-gain'">
|
||||
<div>
|
||||
Current file: <url-link url="image.url" max-length="50"></url-link>
|
||||
<div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url}}" class="checker" /></a></div>
|
||||
</div>
|
||||
<div>
|
||||
<p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
|
||||
<p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
|
||||
<p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
|
||||
<p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
|
||||
<p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
|
||||
<p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'gzipCompression'">
|
||||
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div>File</div>
|
||||
<div>Current weight</div>
|
||||
<div>Gzipped</div>
|
||||
<div>Gain</div>
|
||||
</div>
|
||||
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
|
||||
<div>
|
||||
<url-link url="file.url" max-length="70"></url-link>
|
||||
</div>
|
||||
<div>{{file.original | bytes}}</div>
|
||||
<div>{{file.gzipped | bytes}}</div>
|
||||
<div><b>-{{file.gain | bytes}}</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'fileMinification'">
|
||||
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div>File</div>
|
||||
<div>Current weight</div>
|
||||
<div>Minified</div>
|
||||
<div>Gain</div>
|
||||
</div>
|
||||
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
|
||||
<div>
|
||||
<url-link url="file.url" max-length="70"></url-link>
|
||||
</div>
|
||||
<div ng-if="!file.afterCompression">{{file.original | bytes}} (gzipped)</div>
|
||||
<div ng-if="file.afterCompression">{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)</div>
|
||||
<div ng-if="!file.afterCompression">{{file.afterOptimizationAndCompression | bytes}} (gzipped)</div>
|
||||
<div ng-if="file.afterCompression">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)</div>
|
||||
<div><b>-{{file.gain | bytes}}</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'totalRequests'">
|
||||
<h3>Requests by MIME type</h3>
|
||||
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
|
||||
<h3><ng-pluralize count="requests.length" when="{'0': 'No ' + type + ' request', 'one': '1 ' + type + ' request', 'other': '{} ' + type + ' requests'}"></ng-pluralize></h3>
|
||||
<p ng-if="type == 'css' && requests.length > 2">Reduce the number of stylesheets by concatenating them.</p>
|
||||
<p ng-if="type == 'js' && requests.length > 3">Reduce the number of scripts by concatenating them.</p>
|
||||
<p ng-if="type == 'image' && requests.length > 5">Reduce the number of images by lazyloading them or by spriting them.</p>
|
||||
<p ng-if="type == 'webfont' && requests.length > 1">Fonts are generally loaded on the critical path of the head. Load as few as possible.</p>
|
||||
<p ng-if="type == 'other' && requests.length > 0">They can be Flash, XML, music or any undetected format.</p>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="request in requests track by $index">
|
||||
<div><url-link url="request" max-length="100"></url-link></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'identicalFiles'">
|
||||
<div ng-repeat="offender in rule.offendersObj.list track by $index">
|
||||
<h4>A file of {{offender.weight | bytes}} is loaded {{offender.urls.length}} times:</h4>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="url in offender.urls">
|
||||
<div><url-link url="url" max-length="100"></url-link></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'smallRequests'">
|
||||
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
|
||||
<h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
|
||||
<p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
|
||||
<p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
|
||||
<p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div ng-if="type == 'image'">Preview</div>
|
||||
<div>File</div>
|
||||
<div>Weight (bytes)</div>
|
||||
</div>
|
||||
<div ng-repeat="request in requests track by $index">
|
||||
<div ng-if="type == 'image'"><img ng-src="{{request.url}}" class="smallPreview checker" /></div>
|
||||
<div><url-link url="request.url" max-length="100"></url-link></div>
|
||||
<div>{{request.size}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMaccesses'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline">JS timeline</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'queriesWithoutResults'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssBreakpoints'">
|
||||
<div ng-if="rule.value > 0" class="cssBreakpointsGraph">
|
||||
<h3>Breakpoints distribution graph</h3>
|
||||
<canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-legend="true" chart-colours="breakpointsColours" chart-series="breakpointsSeries" width="600" height="250"></canvas>
|
||||
<h3>Breakpoints list</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
|
||||
<div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="rule.value === 0">
|
||||
No breakpoint
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'http2'">
|
||||
<h3>Protocols advertised by the server</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="protocol in rule.offendersObj.list">
|
||||
<div>{{protocol}}</div>
|
||||
</div>
|
||||
<div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
|
||||
<div>none</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!rule && rule !== null" class="notFound">
|
||||
<h2>404</h2>
|
||||
Rule "{{policyName}}"" not found
|
||||
</div>
|
||||
|
||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="screenshot board">
|
||||
<h2>Screenshot</h2>
|
||||
|
||||
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
|
||||
<div>
|
||||
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>(scroll on the screenshot to see under the fold)</p>
|
||||
|
||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
||||
</div>
|
||||
@@ -1,100 +0,0 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="execution board">
|
||||
<div class="selectScript" ng-class="{empty:!selectedScript}">
|
||||
Filter timeline and profiler by script:
|
||||
<select ng-model="selectedScript" ng-options="script.shortPath for script in scripts" ng-change="changeScript()">
|
||||
<option value="">All (no filter)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h2>JavaScript Timeline</h2>
|
||||
<p>This graph gives a quick view of when the JavaScript interactions with the DOM occur during the loading of the page.</p>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="chart">
|
||||
<div class="chartPoints">
|
||||
<div ng-repeat="duration in timeline track by $index"
|
||||
class="interval"
|
||||
ng-class="{
|
||||
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">
|
||||
<div>
|
||||
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="startTime">0 ms</div>
|
||||
<div class="endTime">{{endTime | number: 0}} ms</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="titles">
|
||||
<div class="domCreation"><div class="color"></div>DOM creation</div>
|
||||
<div class="domInteractive"><div class="color"></div>DOM interactive</div>
|
||||
<div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
|
||||
<div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
|
||||
<div class="domComplete"><div class="color"></div>Page is complete</div>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<div>Executing JavaScript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
|
||||
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
|
||||
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
|
||||
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
|
||||
<div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>JavaScript Profiler</h2>
|
||||
<p>
|
||||
The table below shows the interactions between the JavaScript and the DOM. It is useful to understand what happens while the page loads.
|
||||
</p>
|
||||
<div class="filters">
|
||||
<div>
|
||||
<input type="checkbox" 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="{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>
|
||||
<div>Params</div>
|
||||
<div><!-- details --></div>
|
||||
<div>Timestamp</div>
|
||||
</div>
|
||||
|
||||
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
||||
</div>
|
||||
@@ -1,11 +1,13 @@
|
||||
var Q = require('q');
|
||||
var debug = require('debug')('ylt:index');
|
||||
var Q = require('q');
|
||||
|
||||
var Runner = require('./runner');
|
||||
var Runner = require('./runner');
|
||||
var ScreenshotHandler = require('./screenshotHandler');
|
||||
|
||||
var packageJson = require('../package.json');
|
||||
|
||||
|
||||
var yellowLabTools = function(url, options) {
|
||||
'use strict';
|
||||
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!url) {
|
||||
@@ -24,15 +26,54 @@ var yellowLabTools = function(url, options) {
|
||||
};
|
||||
|
||||
var runner = new Runner(params)
|
||||
.then(function(data) {
|
||||
|
||||
.progress(deferred.notify)
|
||||
|
||||
.then(function(data) {
|
||||
|
||||
// If a screenshot saveFunction was provided in the options
|
||||
if (options && typeof options.saveScreenshotFn === 'function') {
|
||||
debug('Now optimizing screenshot...');
|
||||
|
||||
// TODO: temporarily set all screenshot sizes to 600px, until we find a solution
|
||||
ScreenshotHandler.findAndOptimizeScreenshot(data.params.options.screenshot, 600)
|
||||
|
||||
.then(function(screenshotBuffer) {
|
||||
debug('Screenshot optimized, now saving...');
|
||||
|
||||
return options.saveScreenshotFn('screenshot.jpg', screenshotBuffer);
|
||||
})
|
||||
|
||||
.then(function(response) {
|
||||
debug('Screenshot saved');
|
||||
debug(response);
|
||||
|
||||
// Remove uneeded temp screenshot path
|
||||
delete data.params.options.screenshot;
|
||||
})
|
||||
|
||||
.catch(function(err) {
|
||||
// It's ok if we can't save the screenshot
|
||||
debug('Screenshot could not be saved');
|
||||
debug(err);
|
||||
})
|
||||
|
||||
.finally(function() {
|
||||
deferred.resolve(data);
|
||||
});
|
||||
|
||||
} else {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.fail(function(err) {
|
||||
deferred.reject(err);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
.catch(function(err) {
|
||||
deferred.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
module.exports = yellowLabTools;
|
||||
module.exports = yellowLabTools;
|
||||
module.exports.version = packageJson.version;
|
||||
@@ -1,23 +1,38 @@
|
||||
{
|
||||
"globalScore": {
|
||||
"pageWeight": 2,
|
||||
"images": 2,
|
||||
"domComplexity": 1,
|
||||
"javascriptComplexity": 2,
|
||||
"badJavascript": 2,
|
||||
"jQuery": 0.5,
|
||||
"cssComplexity": 0.5,
|
||||
"badCSS": 1,
|
||||
"fonts": 1,
|
||||
"serverConfig": 1
|
||||
},
|
||||
"categories": {
|
||||
"pageWeight": {
|
||||
"label": "Page weight",
|
||||
"label": "Network",
|
||||
"policies": {
|
||||
"totalWeight": 5,
|
||||
"imageOptimization": 2,
|
||||
"gzipCompression": 2,
|
||||
"fileMinification": 1
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"label": "Requests",
|
||||
"policies": {
|
||||
"totalRequests": 5,
|
||||
"totalRequests": 2,
|
||||
"domains": 3,
|
||||
"notFound": 3,
|
||||
"compression": 2,
|
||||
"fileMinification": 2,
|
||||
"identicalFiles": 2,
|
||||
"emptyRequests": 3,
|
||||
"smallRequests": 1,
|
||||
"notFound": 2
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"label": "Images",
|
||||
"policies": {
|
||||
"imagesNotOptimized": 2,
|
||||
"imagesOldFormat": 1,
|
||||
"imagesScaledDown": 2,
|
||||
"imagesExcessiveDensity": 0.25,
|
||||
"imagesWithIncorrectSizesParam": 1,
|
||||
"lazyLoadableImagesBelowTheFold": 2,
|
||||
"hiddenImages": 1
|
||||
}
|
||||
@@ -31,28 +46,20 @@
|
||||
"DOMidDuplicated": 1
|
||||
}
|
||||
},
|
||||
"domManipulations": {
|
||||
"label": "DOM manipulations",
|
||||
"javascriptComplexity": {
|
||||
"label": "JS complexity",
|
||||
"policies": {
|
||||
"DOMaccesses": 3,
|
||||
"queriesWithoutResults": 1,
|
||||
"DOMqueriesAvoidable": 1
|
||||
}
|
||||
},
|
||||
"scroll": {
|
||||
"label": "Scroll bottlenecks",
|
||||
"policies": {
|
||||
"eventsScrollBound": 1,
|
||||
"DOMaccessesOnScroll": 4
|
||||
"scriptDuration": 4,
|
||||
"DOMaccesses": 2,
|
||||
"eventsScrollBound": 1
|
||||
}
|
||||
},
|
||||
"badJavascript": {
|
||||
"label": "Bad JavaScript",
|
||||
"label": "Bad JS",
|
||||
"policies": {
|
||||
"jsErrors": 1,
|
||||
"documentWriteCalls": 2,
|
||||
"synchronousXHR": 5,
|
||||
"consoleMessages": 0.5,
|
||||
"globalVariables": 0.5
|
||||
}
|
||||
},
|
||||
@@ -60,10 +67,7 @@
|
||||
"label": "jQuery",
|
||||
"policies": {
|
||||
"jQueryVersion": 2,
|
||||
"jQueryVersionsLoaded": 2,
|
||||
"jQueryFunctionsUsed": 1,
|
||||
"jQueryCallsOnEmptyObject": 1,
|
||||
"jQueryNotDelegatedEvents": 1
|
||||
"jQueryVersionsLoaded": 2
|
||||
}
|
||||
},
|
||||
"cssComplexity": {
|
||||
@@ -80,41 +84,37 @@
|
||||
"badCSS": {
|
||||
"label": "Bad CSS",
|
||||
"policies": {
|
||||
"cssParsingErrors": 4,
|
||||
"cssImports": 3,
|
||||
"cssParsingErrors": 3,
|
||||
"cssImports": 4,
|
||||
"cssDuplicatedSelectors": 2,
|
||||
"cssDuplicatedProperties": 1,
|
||||
"cssEmptyRules": 2,
|
||||
"cssExpressions": 1,
|
||||
"cssImportants": 3,
|
||||
"cssImportants": 2,
|
||||
"cssOldIEFixes": 1,
|
||||
"cssOldPropertyPrefixes": 1,
|
||||
"cssRedundantBodySelectors": 1,
|
||||
"cssRedundantChildNodesSelectors": 1
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"label": "Web fonts",
|
||||
"policies": {
|
||||
"fontsCount": 1,
|
||||
"heavyFonts": 0.5,
|
||||
"unusedUnicodeRanges": 0.5,
|
||||
"nonWoff2Fonts": 0.5
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"label": "Server config",
|
||||
"policies": {
|
||||
"http2": 2,
|
||||
"oldHttpProtocol": 2,
|
||||
"oldTlsProtocol": 2,
|
||||
"closedConnections": 2,
|
||||
"cachingNotSpecified": 1,
|
||||
"cachingDisabled": 1,
|
||||
"cachingTooShort": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"globalScore": {
|
||||
"pageWeight": 3,
|
||||
"requests": 3,
|
||||
"domComplexity": 2,
|
||||
"domManipulations": 2,
|
||||
"scroll": 1,
|
||||
"badJavascript": 2,
|
||||
"jQuery": 1,
|
||||
"cssSyntaxError": 1,
|
||||
"cssComplexity": 1,
|
||||
"badCSS": 1,
|
||||
"serverConfig": 1
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ var OffendersHelpers = function() {
|
||||
|
||||
for (var i=0 ; i<traceArray.length ; i++) {
|
||||
// Handle the new PhantomJS 2.x syntax
|
||||
parts = /^(([\w$]+)@)?([^ ]+):(\d+):(\d+)$/.exec(traceArray[i]);
|
||||
parts = /^\s*at( (\w+))? \(?([^ ]+):(\d+):(\d+)\)?$$/.exec(traceArray[i]);
|
||||
|
||||
if (parts) {
|
||||
obj = {
|
||||
@@ -132,25 +132,6 @@ var OffendersHelpers = function() {
|
||||
|
||||
results.push(obj);
|
||||
|
||||
} else {
|
||||
// Old syntax
|
||||
parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
|
||||
|
||||
if (parts) {
|
||||
obj = {
|
||||
file: parts[3],
|
||||
line: parseInt(parts[4], 10)
|
||||
};
|
||||
|
||||
if (parts[2]) {
|
||||
obj.functionName = parts[2];
|
||||
}
|
||||
|
||||
results.push(obj);
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
@@ -172,23 +153,15 @@ 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) {
|
||||
return {
|
||||
offender: offender
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
css: parts[1],
|
||||
file: parts[2] || null,
|
||||
line: parseInt(parts[3], 10),
|
||||
column: parseInt(parts[4], 10)
|
||||
};
|
||||
}
|
||||
// Used to work with strings
|
||||
// As of Phantomas v2, offender is now in JSON format.
|
||||
// Let's just adapt this for now and we'll see later if we remove completely this function
|
||||
return {
|
||||
css: offender.value.message,
|
||||
file: offender.url,
|
||||
line: offender.value.position.start.line,
|
||||
column: offender.value.position.start.column
|
||||
};
|
||||
};
|
||||
|
||||
this.fileWithSizePattern = function(fileWithSize) {
|
||||
@@ -207,24 +180,31 @@ var OffendersHelpers = function() {
|
||||
};
|
||||
|
||||
this.orderByFile = function(offenders) {
|
||||
var byFile = {};
|
||||
var byFileObj = {};
|
||||
|
||||
offenders.forEach(function(offender) {
|
||||
var file = offender.file || 'Inline CSS';
|
||||
delete offender.file;
|
||||
|
||||
if (!byFile[file]) {
|
||||
byFile[file] = {
|
||||
if (!byFileObj[file]) {
|
||||
byFileObj[file] = {
|
||||
url: file,
|
||||
count: 0,
|
||||
offenders: []
|
||||
};
|
||||
}
|
||||
|
||||
byFile[file].count ++;
|
||||
byFile[file].offenders.push(offender);
|
||||
byFileObj[file].count ++;
|
||||
byFileObj[file].offenders.push(offender);
|
||||
});
|
||||
|
||||
return {byFile: byFile};
|
||||
// Transform object into array
|
||||
var byFileArray = [];
|
||||
for (var file in byFileObj) {
|
||||
byFileArray.push(byFileObj[file]);
|
||||
}
|
||||
|
||||
return {byFile: byFileArray};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
@@ -57,9 +57,16 @@ var RulesChecker = function() {
|
||||
data.toolsResults[policy.tool].offenders[metricName]) {
|
||||
offenders = data.toolsResults[policy.tool].offenders[metricName];
|
||||
}
|
||||
|
||||
// It is possible to declare a transformation function for the main metric value.
|
||||
// The function should
|
||||
if (policy.valueTransformFn) {
|
||||
rule.value = policy.valueTransformFn(offenders);
|
||||
}
|
||||
|
||||
|
||||
var offendersObj = {};
|
||||
|
||||
|
||||
// It is possible to declare a transformation function for the offenders.
|
||||
// The function should take an array of strings as single parameter and return a string.
|
||||
if (policy.offendersTransformFn) {
|
||||
@@ -111,7 +118,8 @@ var RulesChecker = function() {
|
||||
if (rule) {
|
||||
rule.policy = {
|
||||
label: policy.label,
|
||||
message: policy.message
|
||||
message: policy.message,
|
||||
hasOffenders: policy.hasOffenders || false
|
||||
};
|
||||
|
||||
results[metricName] = rule;
|
||||
|
||||
@@ -2,10 +2,9 @@ var Q = require('q');
|
||||
var debug = require('debug')('ylt:runner');
|
||||
|
||||
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
|
||||
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
|
||||
var colorDiff = require('./tools/colorDiff');
|
||||
var domAccessAgregator = require('./tools/domAccessAgregator');
|
||||
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
|
||||
var isHttp2 = require('./tools/isHttp2');
|
||||
var redownload = require('./tools/redownload/redownload');
|
||||
var rulesChecker = require('./rulesChecker');
|
||||
var scoreCalculator = require('./scoreCalculator');
|
||||
@@ -25,11 +24,33 @@ var Runner = function(params) {
|
||||
// Execute Phantomas first
|
||||
phantomasWrapper.execute(data)
|
||||
|
||||
// For the progress bar
|
||||
.progress(function(event) {
|
||||
if (event === 'domReady') {
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.15,
|
||||
milestone: 'domReady'
|
||||
});
|
||||
}
|
||||
if (event === 'domComplete') {
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.25,
|
||||
milestone: 'domComplete'
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
.then(function(phantomasResults) {
|
||||
// For the progress bar
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.4,
|
||||
milestone: 'phantomas'
|
||||
});
|
||||
|
||||
data.toolsResults.phantomas = phantomasResults;
|
||||
|
||||
// Treat the JS Execution Tree from offenders
|
||||
data = jsExecutionTransformer.transform(data);
|
||||
// Mix all DOM Access metrics together
|
||||
data = domAccessAgregator.agregate(data);
|
||||
|
||||
// Compare colors
|
||||
data = colorDiff.compareAllColors(data);
|
||||
@@ -38,16 +59,30 @@ var Runner = function(params) {
|
||||
data = mediaQueriesChecker.analyzeMediaQueries(data);
|
||||
|
||||
// Redownload every file
|
||||
return redownload.recheckAllFiles(data);
|
||||
return redownload.recheckAllFiles(data)
|
||||
|
||||
.progress(function(redownloadedProgress) {
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.4 + redownloadedProgress * 0.55,
|
||||
milestone: 'phantomas'
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
// Check if HTTP2
|
||||
return isHttp2.check(data);
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
// For the progress bar
|
||||
deferred.notify({
|
||||
estimatedProgress: 0.99,
|
||||
milestone: 'redownload'
|
||||
});
|
||||
|
||||
// Fix: don't display Unicode ranges if the module is not present in Phantomas
|
||||
if (!data.toolsResults.phantomas.metrics.differentCharacters) {
|
||||
delete data.toolsResults.redownload.metrics.unusedUnicodeRanges;
|
||||
delete data.toolsResults.redownload.offenders.unusedUnicodeRanges;
|
||||
}
|
||||
|
||||
// Rules checker
|
||||
var policies = require('./metadata/policies');
|
||||
@@ -57,20 +92,36 @@ var Runner = function(params) {
|
||||
// Scores calculator
|
||||
var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
|
||||
data.scoreProfiles = {
|
||||
generic : scoreCalculator.calculate(data, scoreProfileGeneric)
|
||||
generic : scoreCalculator.calculate(data, scoreProfileGeneric, true)
|
||||
};
|
||||
|
||||
|
||||
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
|
||||
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
|
||||
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
|
||||
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
|
||||
|
||||
// Calculate "If you fix this issue, your new score is..." on each rule
|
||||
debug('Calculate "If you fix this issue..." scores');
|
||||
Object.keys(data.rules).forEach(function(ruleName) {
|
||||
// Save current values
|
||||
var oldScore = data.rules[ruleName].score;
|
||||
var oldAbnormalityScore = data.rules[ruleName].abnormalityScore;
|
||||
// Simulate a 100/100 score on a specific rule
|
||||
data.rules[ruleName].score = 100;
|
||||
data.rules[ruleName].abnormalityScore = 0;
|
||||
// Calculate new score
|
||||
data.rules[ruleName].globalScoreIfFixed = scoreCalculator.calculate(data, scoreProfileGeneric, false).globalScore;
|
||||
// Revert values
|
||||
data.rules[ruleName].score = oldScore;
|
||||
data.rules[ruleName].abnormalityScore = oldAbnormalityScore;
|
||||
});
|
||||
debug('Calculating is finished');
|
||||
|
||||
|
||||
if (data.toolsResults.phantomas.offenders.blockedRequests) {
|
||||
data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
|
||||
}
|
||||
|
||||
// Report WordPress detection
|
||||
if (data.toolsResults.redownload.metrics.isWordPress === true) {
|
||||
data.frameworks = {isWordPress: true};
|
||||
}
|
||||
|
||||
// Finished!
|
||||
deferred.resolve(data);
|
||||
|
||||
@@ -4,7 +4,7 @@ var debug = require('debug')('ylt:scoreCalculator');
|
||||
var ScoreCalculator = function() {
|
||||
'use strict';
|
||||
|
||||
this.calculate = function(data, profile) {
|
||||
this.calculate = function(data, profile, debugFlag) {
|
||||
|
||||
var results = {
|
||||
categories: {}
|
||||
@@ -13,7 +13,9 @@ var ScoreCalculator = function() {
|
||||
var categoryName;
|
||||
var weight;
|
||||
|
||||
debug('Starting calculating scores');
|
||||
if (debugFlag) {
|
||||
debug('Starting calculating scores');
|
||||
}
|
||||
|
||||
// Calculate categories
|
||||
for (categoryName in profile.categories) {
|
||||
@@ -31,7 +33,7 @@ var ScoreCalculator = function() {
|
||||
if (data.rules[policyName]) {
|
||||
policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
|
||||
categoryScore.push(policyScore, weight);
|
||||
} else {
|
||||
} else if (debugFlag) {
|
||||
debug('Warning: could not find rule %s', policyName);
|
||||
}
|
||||
|
||||
@@ -59,8 +61,10 @@ var ScoreCalculator = function() {
|
||||
results.globalScore = Math.round(globalScore.getScore());
|
||||
|
||||
|
||||
debug('Score calculation finished:');
|
||||
debug(results);
|
||||
if (debugFlag) {
|
||||
debug('Score calculation finished:');
|
||||
debug(results);
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
var debug = require('debug')('ylt:screenshotHandler');
|
||||
var lwip = require('lwip');
|
||||
var tmp = require('temporary');
|
||||
var debug = require('debug')('ylt:screenshotHandlerAgent');
|
||||
var Jimp = require('jimp');
|
||||
var Q = require('q');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
@@ -8,48 +7,18 @@ var path = require('path');
|
||||
|
||||
var screenshotHandler = function() {
|
||||
|
||||
this.getScreenshotTempFile = function() {
|
||||
|
||||
var screenshotTmpFolder = new tmp.Dir();
|
||||
var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.png');
|
||||
var that = this;
|
||||
|
||||
return {
|
||||
|
||||
getTmpFolder: function() {
|
||||
return screenshotTmpFolder;
|
||||
},
|
||||
|
||||
getTmpFilePath: function() {
|
||||
return tmpFilePath;
|
||||
},
|
||||
|
||||
toThumbnail: function(width) {
|
||||
return that.optimize(tmpFilePath, width);
|
||||
},
|
||||
|
||||
deleteTmpFile: function() {
|
||||
return that.deleteTmpFileAndFolder(tmpFilePath, screenshotTmpFolder);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
this.optimize = function(imagePath, width) {
|
||||
this.findAndOptimizeScreenshot = function(tmpScreenshotPath, width) {
|
||||
var that = this;
|
||||
|
||||
debug('Starting screenshot transformation');
|
||||
|
||||
return this.openImage(imagePath)
|
||||
return this.openImage(tmpScreenshotPath)
|
||||
|
||||
.then(function(image) {
|
||||
|
||||
that.deleteTmpFile(tmpScreenshotPath);
|
||||
return that.resizeImage(image, width);
|
||||
|
||||
})
|
||||
|
||||
.then(this.addWhiteBackground)
|
||||
|
||||
.then(this.toBuffer);
|
||||
};
|
||||
|
||||
@@ -57,7 +26,7 @@ var screenshotHandler = function() {
|
||||
this.openImage = function(imagePath) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
lwip.open(imagePath, function(err, image){
|
||||
Jimp.read(imagePath, function(err, image){
|
||||
if (err) {
|
||||
debug('Could not open imagePath %s', imagePath);
|
||||
debug(err);
|
||||
@@ -76,62 +45,34 @@ var screenshotHandler = function() {
|
||||
this.resizeImage = function(image, newWidth) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
var currentWidth = image.width();
|
||||
var ratio = newWidth / currentWidth;
|
||||
var currentWidth = image.bitmap.width;
|
||||
|
||||
image.scale(ratio, function(err, image){
|
||||
if (err) {
|
||||
debug('Could not resize image');
|
||||
debug(err);
|
||||
if (currentWidth > 0) {
|
||||
var ratio = newWidth / currentWidth;
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
debug('Image correctly resized');
|
||||
deferred.resolve(image);
|
||||
}
|
||||
});
|
||||
image.scale(ratio, function(err, image){
|
||||
if (err) {
|
||||
debug('Could not resize image');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
debug('Image correctly resized');
|
||||
deferred.resolve(image);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deferred.reject('Could not resize an empty image');
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
||||
image.toBuffer('jpg', {quality: 90}, function(err, buffer){
|
||||
image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
|
||||
if (err) {
|
||||
debug('Could not save image to buffer');
|
||||
debug(err);
|
||||
@@ -143,23 +84,20 @@ var screenshotHandler = function() {
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
this.deleteTmpFileAndFolder = function(tmpFilePath, screenshotTmpFolder) {
|
||||
this.deleteTmpFile = function(tmpFilePath) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
fs.unlink(tmpFilePath, function (err) {
|
||||
if (err) {
|
||||
debug('Screenshot file not found, could not be deleted. But it is not a problem.');
|
||||
debug('Screenshot temporary file not found, could not be deleted. But it is not a problem.');
|
||||
} else {
|
||||
debug('Screenshot file deleted.');
|
||||
debug('Screenshot temporary file deleted.');
|
||||
}
|
||||
|
||||
screenshotTmpFolder.rmdir();
|
||||
debug('Screenshot temp folder deleted');
|
||||
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
@@ -167,4 +105,4 @@ var screenshotHandler = function() {
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new screenshotHandler();
|
||||
module.exports = new screenshotHandler();
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
var debug = require('debug')('ylt:server');
|
||||
var Q = require('q');
|
||||
|
||||
var ylt = require('../../index');
|
||||
var ScreenshotHandler = require('../../screenshotHandler');
|
||||
var RunsQueue = require('../datastores/runsQueue');
|
||||
var RunsDatastore = require('../datastores/runsDatastore');
|
||||
var ResultsDatastore = require('../datastores/resultsDatastore');
|
||||
var serverSettings = require('../../../server_config/settings.json');
|
||||
|
||||
var ApiController = function(app) {
|
||||
'use strict';
|
||||
|
||||
var queue = new RunsQueue();
|
||||
var runsDatastore = new RunsDatastore();
|
||||
var resultsDatastore = new ResultsDatastore();
|
||||
|
||||
// Create a new run
|
||||
app.post('/api/runs', function(req, res) {
|
||||
|
||||
// Add http to the test URL
|
||||
if (req.body.url && req.body.url.toLowerCase().indexOf('http://') !== 0 && req.body.url.toLowerCase().indexOf('https://') !== 0) {
|
||||
req.body.url = 'http://' + req.body.url;
|
||||
}
|
||||
|
||||
// Grab the test parameters and generate a random run ID
|
||||
var run = {
|
||||
runId: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36),
|
||||
params: {
|
||||
url: req.body.url,
|
||||
waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
|
||||
partialResult: req.body.partialResult || null,
|
||||
screenshot: req.body.screenshot || false,
|
||||
device: req.body.device || 'desktop',
|
||||
waitForSelector: req.body.waitForSelector || null,
|
||||
cookie: req.body.cookie || null,
|
||||
authUser: req.body.authUser || null,
|
||||
authPass: req.body.authPass || null,
|
||||
blockDomain: req.body.blockDomain || null,
|
||||
allowDomain: req.body.allowDomain || null,
|
||||
noExternals: req.body.noExternals || false
|
||||
}
|
||||
};
|
||||
|
||||
// Create a temporary folder to save the screenshot
|
||||
var screenshot;
|
||||
if (run.params.screenshot) {
|
||||
screenshot = ScreenshotHandler.getScreenshotTempFile();
|
||||
}
|
||||
|
||||
// Add test to the testQueue
|
||||
debug('Adding test %s to the queue', run.runId);
|
||||
var queuePromise = queue.push(run.runId);
|
||||
|
||||
// Save the run to the datastore
|
||||
runsDatastore.add(run, queuePromise.startingPosition);
|
||||
|
||||
|
||||
// Listening for position updates
|
||||
queuePromise.progress(function(position) {
|
||||
runsDatastore.updatePosition(run.runId, position);
|
||||
});
|
||||
|
||||
// Let's start the run
|
||||
queuePromise.then(function() {
|
||||
|
||||
runsDatastore.updatePosition(run.runId, 0);
|
||||
|
||||
debug('Launching test %s on %s', run.runId, run.params.url);
|
||||
|
||||
var runOptions = {
|
||||
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
|
||||
device: run.params.device,
|
||||
waitForSelector: run.params.waitForSelector,
|
||||
cookie: run.params.cookie,
|
||||
authUser: run.params.authUser,
|
||||
authPass: run.params.authPass,
|
||||
blockDomain: run.params.blockDomain,
|
||||
allowDomain: run.params.allowDomain,
|
||||
noExternals: run.params.noExternals,
|
||||
phantomasEngine: serverSettings.phantomasEngine
|
||||
};
|
||||
|
||||
return ylt(run.params.url, runOptions);
|
||||
|
||||
})
|
||||
// Phantomas completed, let's save the screenshot if any
|
||||
.then(function(data) {
|
||||
|
||||
debug('Success');
|
||||
data.runId = run.runId;
|
||||
|
||||
|
||||
// Some conditional steps are made if there is a screenshot
|
||||
var screenshotPromise = Q.resolve();
|
||||
|
||||
if (run.params.screenshot) {
|
||||
|
||||
// Replace the empty promise created earlier with Q.resolve()
|
||||
screenshotPromise = screenshot.toThumbnail(serverSettings.screenshotWidth || 400)
|
||||
|
||||
// Read screenshot
|
||||
.then(function(screenshotBuffer) {
|
||||
|
||||
if (screenshotBuffer) {
|
||||
debug('Image optimized');
|
||||
data.screenshotBuffer = screenshotBuffer;
|
||||
|
||||
// Official path to get the image
|
||||
data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// Delete screenshot temporary file
|
||||
.then(screenshot.deleteTmpFile)
|
||||
|
||||
// Don't worry if there's an error
|
||||
.fail(function(err) {
|
||||
debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Let's continue
|
||||
screenshotPromise
|
||||
|
||||
// Save results
|
||||
.then(function() {
|
||||
// Remove uneeded temp screenshot path
|
||||
delete data.params.options.screenshot;
|
||||
|
||||
// Here we can remove tools results if not needed
|
||||
|
||||
return resultsDatastore.saveResult(data);
|
||||
})
|
||||
|
||||
// Mark as the run as complete and send the response if the request is still waiting
|
||||
.then(function() {
|
||||
|
||||
debug('Result saved in datastore');
|
||||
|
||||
runsDatastore.markAsComplete(run.runId);
|
||||
|
||||
if (run.params.waitForResponse) {
|
||||
|
||||
// If the user only wants a portion of the result (partialResult option)
|
||||
switch(run.params.partialResult) {
|
||||
case 'generalScores':
|
||||
res.redirect(302, '/api/results/' + run.runId + '/generalScores');
|
||||
break;
|
||||
case 'rules':
|
||||
res.redirect(302, '/api/results/' + run.runId + '/rules');
|
||||
break;
|
||||
case 'javascriptExecutionTree':
|
||||
res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree');
|
||||
break;
|
||||
case 'phantomas':
|
||||
res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas');
|
||||
break;
|
||||
default:
|
||||
res.redirect(302, '/api/results/' + run.runId);
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.fail(function(err) {
|
||||
console.error('Test failed for URL: %s', run.params.url);
|
||||
console.error(err.toString());
|
||||
|
||||
runsDatastore.markAsFailed(run.runId, err.toString());
|
||||
|
||||
res.status(500).send('An error occured');
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
|
||||
console.error('Test failed for URL: %s', run.params.url);
|
||||
console.error(err.toString());
|
||||
|
||||
runsDatastore.markAsFailed(run.runId, err.toString());
|
||||
|
||||
res.status(400).send('Bad request');
|
||||
|
||||
})
|
||||
|
||||
.finally(function() {
|
||||
queue.remove(run.runId);
|
||||
});
|
||||
|
||||
|
||||
// The user doesn't want to wait for the response, sending the run ID only
|
||||
if (!run.params.waitForResponse) {
|
||||
console.log('Sending response without waiting.');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify({runId: run.runId}));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Retrive one run by id
|
||||
app.get('/api/runs/:id', function(req, res) {
|
||||
var runId = req.params.id;
|
||||
|
||||
var run = runsDatastore.get(runId);
|
||||
|
||||
if (run) {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify(run, null, 2));
|
||||
} else {
|
||||
res.status(404).send('Not found');
|
||||
}
|
||||
});
|
||||
|
||||
// Retrieve the list of all runs
|
||||
/*app.get('/api/runs', function(req, res) {
|
||||
// NOT YET
|
||||
});*/
|
||||
|
||||
// Delete one run by id
|
||||
/*app.delete('/api/runs/:id', function(req, res) {
|
||||
deleteRun()
|
||||
});*/
|
||||
|
||||
// Delete all
|
||||
/*app.delete('/api/runs', function(req, res) {
|
||||
purgeRuns()
|
||||
});
|
||||
|
||||
// List all
|
||||
app.get('/api/runs', function(req, res) {
|
||||
listRuns()
|
||||
});
|
||||
|
||||
// Exists
|
||||
app.head('/api/runs/:id', function(req, res) {
|
||||
existsX();
|
||||
// Returns 200 if the result exists or 404 if not
|
||||
});
|
||||
*/
|
||||
|
||||
// Retrive one result by id
|
||||
app.get('/api/results/:id', function(req, res) {
|
||||
getPartialResults(req.params.id, res, function(data) {
|
||||
|
||||
// Some fields can be excluded from the response, this way:
|
||||
// /api/results/:id?exclude=field1,field2
|
||||
if (req.query.exclude && typeof req.query.exclude === 'string') {
|
||||
var excludedFields = req.query.exclude.split(',');
|
||||
excludedFields.forEach(function(fieldName) {
|
||||
if (data[fieldName]) {
|
||||
delete data[fieldName];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
});
|
||||
|
||||
// Retrieve one result and return only the generalScores part of the response
|
||||
app.get('/api/results/:id/generalScores', function(req, res) {
|
||||
getPartialResults(req.params.id, res, function(data) {
|
||||
return data.scoreProfiles.generic;
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/results/:id/generalScores/:scoreProfile', function(req, res) {
|
||||
getPartialResults(req.params.id, res, function(data) {
|
||||
return data.scoreProfiles[req.params.scoreProfile];
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/results/:id/rules', function(req, res) {
|
||||
getPartialResults(req.params.id, res, function(data) {
|
||||
return data.rules;
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/results/:id/javascriptExecutionTree', function(req, res) {
|
||||
getPartialResults(req.params.id, res, function(data) {
|
||||
return data.javascriptExecutionTree;
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/results/:id/toolsResults/phantomas', function(req, res) {
|
||||
getPartialResults(req.params.id, res, function(data) {
|
||||
return data.toolsResults.phantomas;
|
||||
});
|
||||
});
|
||||
|
||||
function getPartialResults(runId, res, partialGetterFn) {
|
||||
resultsDatastore.getResult(runId)
|
||||
.then(function(data) {
|
||||
var results = partialGetterFn(data);
|
||||
|
||||
if (typeof results === 'undefined') {
|
||||
res.status(404).send('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify(results, null, 2));
|
||||
|
||||
}).fail(function() {
|
||||
res.status(404).send('Not found');
|
||||
});
|
||||
}
|
||||
|
||||
// Retrive one result by id
|
||||
app.get('/api/results/:id/screenshot.jpg', function(req, res) {
|
||||
var runId = req.params.id;
|
||||
|
||||
resultsDatastore.getScreenshot(runId)
|
||||
.then(function(screenshotBuffer) {
|
||||
|
||||
res.setHeader('Content-Type', 'image/jpeg');
|
||||
res.send(screenshotBuffer);
|
||||
|
||||
}).fail(function() {
|
||||
res.status(404).send('Not found');
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports = ApiController;
|
||||
@@ -1,26 +0,0 @@
|
||||
var path = require('path');
|
||||
var express = require('express');
|
||||
|
||||
var FrontController = function(app) {
|
||||
'use strict';
|
||||
|
||||
var cacheDuration = 365 * 24 * 60 * 60 * 1000; // One year
|
||||
var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
|
||||
|
||||
var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
|
||||
routes.forEach(function(route) {
|
||||
app.get(route, function(req, res) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=20');
|
||||
res.sendFile(path.join(__dirname, assetsPath, 'main.html'));
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/css', express.static(path.join(__dirname, assetsPath, 'css'), { maxAge: cacheDuration }));
|
||||
app.use('/fonts', express.static(path.join(__dirname, assetsPath, 'fonts'), { maxAge: cacheDuration }));
|
||||
app.use('/img', express.static(path.join(__dirname, assetsPath, 'img'), { maxAge: cacheDuration }));
|
||||
app.use('/js', express.static(path.join(__dirname, assetsPath, 'js'), { maxAge: cacheDuration }));
|
||||
app.use('/views', express.static(path.join(__dirname, assetsPath, 'views'), { maxAge: cacheDuration }));
|
||||
app.use('/node_modules', express.static(path.join(__dirname, '../../../node_modules'), { maxAge: cacheDuration }));
|
||||
};
|
||||
|
||||
module.exports = FrontController;
|
||||
@@ -1,120 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var rimraf = require('rimraf');
|
||||
var path = require('path');
|
||||
var Q = require('q');
|
||||
var debug = require('debug')('ylt:resultsDatastore');
|
||||
|
||||
|
||||
function ResultsDatastore() {
|
||||
'use strict';
|
||||
|
||||
var resultFileName = 'results.json';
|
||||
var resultScreenshotName = 'screenshot.jpg';
|
||||
var resultsFolderName = 'results';
|
||||
var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName);
|
||||
|
||||
|
||||
this.saveResult = function(testResults) {
|
||||
|
||||
return createResultFolder(testResults.runId)
|
||||
|
||||
.then(function() {
|
||||
return saveScreenshotIfExists(testResults);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
|
||||
debug('Saving results to disk...');
|
||||
|
||||
var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName);
|
||||
debug('Destination file is %s', resultFilePath);
|
||||
|
||||
return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
this.getResult = function(runId) {
|
||||
|
||||
var resultFilePath = path.join(resultsDir, runId, resultFileName);
|
||||
|
||||
debug('Reading results (runID = %s) from disk...', runId);
|
||||
|
||||
return Q.nfcall(fs.readFile, resultFilePath, {encoding: 'utf8'}).then(function(data) {
|
||||
return JSON.parse(data);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
this.deleteResult = function(runId) {
|
||||
var folder = path.join(resultsDir, runId);
|
||||
|
||||
debug('Deleting results (runID = %s) from disk...', runId);
|
||||
|
||||
return Q.nfcall(rimraf, folder);
|
||||
};
|
||||
|
||||
|
||||
// The folder /results/folderName/
|
||||
function createResultFolder(folderName) {
|
||||
var folder = path.join(resultsDir, folderName);
|
||||
|
||||
debug('Creating the folder %s', folderName);
|
||||
|
||||
return createGlobalFolder().then(function() {
|
||||
return Q.nfcall(fs.mkdir, folder);
|
||||
});
|
||||
}
|
||||
|
||||
// The folder /results/
|
||||
function createGlobalFolder() {
|
||||
var deferred = Q.defer();
|
||||
|
||||
// Create the results folder if it doesn't exist
|
||||
fs.exists(resultsDir, function(exists) {
|
||||
if (exists) {
|
||||
deferred.resolve();
|
||||
} else {
|
||||
debug('Creating the global results folder', resultsDir);
|
||||
fs.mkdir(resultsDir, function(err) {
|
||||
if (err) {
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// If there is a screenshot, save it as screenshot.jpg in the same folder as the results
|
||||
function saveScreenshotIfExists(testResults) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (testResults.screenshotBuffer) {
|
||||
|
||||
var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
|
||||
fs.writeFile(screenshotFilePath, testResults.screenshotBuffer);
|
||||
|
||||
delete testResults.screenshotBuffer;
|
||||
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
return deferred;
|
||||
}
|
||||
|
||||
this.getScreenshot = function(runId) {
|
||||
|
||||
var screenshotFilePath = path.join(resultsDir, runId, resultScreenshotName);
|
||||
|
||||
debug('Getting screenshot (runID = %s) from disk...', runId);
|
||||
|
||||
return Q.nfcall(fs.readFile, screenshotFilePath);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = ResultsDatastore;
|
||||
@@ -1,112 +0,0 @@
|
||||
|
||||
|
||||
function RunsDatastore() {
|
||||
'use strict';
|
||||
|
||||
// NOT PERSISTING RUNS
|
||||
// For the moment, maybe one day
|
||||
var runs = {};
|
||||
|
||||
var STATUS_AWAITING = 'awaiting';
|
||||
var STATUS_RUNNING = 'running';
|
||||
var STATUS_COMPLETE = 'complete';
|
||||
var STATUS_FAILED = 'failed';
|
||||
|
||||
|
||||
this.add = function(run, position) {
|
||||
runs[run.runId] = run;
|
||||
this.updatePosition(run.runId, position);
|
||||
};
|
||||
|
||||
|
||||
this.get = function(runId) {
|
||||
return runs[runId];
|
||||
};
|
||||
|
||||
|
||||
this.updatePosition = function(runId, position) {
|
||||
var run = runs[runId];
|
||||
|
||||
if (position > 0) {
|
||||
run.status = {
|
||||
statusCode: STATUS_AWAITING,
|
||||
position: position
|
||||
};
|
||||
} else {
|
||||
run.status = {
|
||||
statusCode: STATUS_RUNNING
|
||||
};
|
||||
}
|
||||
|
||||
runs[runId] = run;
|
||||
};
|
||||
|
||||
|
||||
this.markAsComplete = function(runId) {
|
||||
var run = runs[runId];
|
||||
|
||||
run.status = {
|
||||
statusCode: STATUS_COMPLETE
|
||||
};
|
||||
|
||||
runs[runId] = run;
|
||||
};
|
||||
|
||||
|
||||
this.markAsFailed = function(runId, err) {
|
||||
var run = runs[runId];
|
||||
|
||||
var errorMessage;
|
||||
switch(err) {
|
||||
case '1':
|
||||
errorMessage = "Error 1: unknown error";
|
||||
break;
|
||||
case '252':
|
||||
errorMessage = "Error 252: page timeout in Phantomas";
|
||||
break;
|
||||
case '253':
|
||||
errorMessage = "Error 253: Phantomas config error";
|
||||
break;
|
||||
case '254':
|
||||
errorMessage = "Error 254: page loading failed in PhantomJS";
|
||||
break;
|
||||
case '255':
|
||||
errorMessage = "Error 255: Phantomas error";
|
||||
break;
|
||||
case '1001':
|
||||
errorMessage = "Error 1001: JavaScript profiling failed";
|
||||
break;
|
||||
case '1002':
|
||||
errorMessage = "Error 1002: missing Phantomas metrics";
|
||||
break;
|
||||
case '1003':
|
||||
errorMessage = "Error 1003: Phantomas not returning";
|
||||
break;
|
||||
default:
|
||||
errorMessage = err;
|
||||
}
|
||||
|
||||
run.status = {
|
||||
statusCode: STATUS_FAILED,
|
||||
error: errorMessage
|
||||
};
|
||||
|
||||
runs[runId] = run;
|
||||
};
|
||||
|
||||
|
||||
this.delete = function(runId) {
|
||||
delete runs[runId];
|
||||
};
|
||||
|
||||
|
||||
this.list = function() {
|
||||
var runsArray = [];
|
||||
Object.keys(runs).forEach(function(key) {
|
||||
runsArray.push(runs[key]);
|
||||
});
|
||||
return runsArray;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = RunsDatastore;
|
||||
@@ -1,83 +0,0 @@
|
||||
var Q = require('q');
|
||||
var debug = require('debug')('ylt:runsQueue');
|
||||
|
||||
|
||||
function RunsQueue() {
|
||||
'use strict';
|
||||
|
||||
var queue = [];
|
||||
|
||||
|
||||
this.push = function(runId) {
|
||||
var deferred = Q.defer();
|
||||
var startingPosition = queue.length;
|
||||
|
||||
debug('Adding run %s to the queue, position is %d', runId, startingPosition);
|
||||
|
||||
if (startingPosition === 0) {
|
||||
|
||||
// The queue is empty, let's run immediatly
|
||||
queue.push({
|
||||
runId: runId
|
||||
});
|
||||
|
||||
deferred.resolve();
|
||||
|
||||
} else {
|
||||
|
||||
queue.push({
|
||||
runId: runId,
|
||||
positionChangedCallback: function(position) {
|
||||
deferred.notify(position);
|
||||
},
|
||||
itIsTimeCallback: function() {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var promise = deferred.promise;
|
||||
promise.startingPosition = startingPosition;
|
||||
return promise;
|
||||
};
|
||||
|
||||
|
||||
this.getPosition = function(runId) {
|
||||
// Position 0 means it's a work in progress (a run is removed AFTER it is finished, not before)
|
||||
var position = -1;
|
||||
|
||||
queue.some(function(run, index) {
|
||||
if (run.runId === runId) {
|
||||
position = index;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return position;
|
||||
};
|
||||
|
||||
|
||||
this.remove = function(runId) {
|
||||
var position = this.getPosition(runId);
|
||||
if (position >= 0) {
|
||||
queue.splice(position, 1);
|
||||
}
|
||||
|
||||
// Update other runs' positions
|
||||
queue.forEach(function(run, index) {
|
||||
if (index === 0 && run.itIsTimeCallback) {
|
||||
run.itIsTimeCallback();
|
||||
} else if (index > 0 && run.positionChangedCallback) {
|
||||
run.positionChangedCallback(index);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
this.length = function() {
|
||||
return queue.length;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = RunsQueue;
|
||||
@@ -1,85 +0,0 @@
|
||||
var config = require('../../../server_config/settings.json');
|
||||
|
||||
var debug = require('debug')('apiLimitsMiddleware');
|
||||
|
||||
|
||||
var apiLimitsMiddleware = function(req, res, next) {
|
||||
'use strict';
|
||||
|
||||
debug('Entering API Limits Middleware with IP address %s', req.connection.remoteAddress);
|
||||
|
||||
if (req.path.indexOf('/api/') === 0 && !res.locals.hasApiKey) {
|
||||
|
||||
|
||||
if (req.path === '/api/runs') {
|
||||
|
||||
if (!runsTable.accepts(req.connection.remoteAddress)) {
|
||||
// Sorry :/
|
||||
debug('Too many tests launched from IP address %s', req.connection.remoteAddress);
|
||||
res.status(429).send('Too many requests');
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!callsTable.accepts(req.connection.remoteAddress)) {
|
||||
// Sorry :/
|
||||
debug('Too many API requests from IP address %s', req.connection.remoteAddress);
|
||||
res.status(429).send('Too many requests');
|
||||
return;
|
||||
}
|
||||
|
||||
debug('Not blocked by the API limits');
|
||||
// It's ok for the moment
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
var RecordTable = function(maxPerDay) {
|
||||
var table = {};
|
||||
|
||||
// Check if the user overpassed the limit and save its visit
|
||||
this.accepts = function(ipAddress) {
|
||||
if (table[ipAddress]) {
|
||||
|
||||
this.cleanEntry(ipAddress);
|
||||
|
||||
debug('%d visits in the last 24 hours', table[ipAddress].length);
|
||||
|
||||
if (table[ipAddress].length >= maxPerDay) {
|
||||
return false;
|
||||
} else {
|
||||
table[ipAddress].push(Date.now());
|
||||
}
|
||||
|
||||
} else {
|
||||
table[ipAddress] = [];
|
||||
table[ipAddress].push(Date.now());
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Clean the table for this guy
|
||||
this.cleanEntry = function(ipAddress) {
|
||||
table[ipAddress] = table[ipAddress].filter(function(date) {
|
||||
return date > Date.now() - 1000*60*60*24;
|
||||
});
|
||||
};
|
||||
|
||||
// Clean the entire table once in a while
|
||||
this.removeOld = function() {
|
||||
for (var ipAddress in table) {
|
||||
this.cleanEntry(ipAddress);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// Init the records tables
|
||||
var runsTable = new RecordTable(config.maxAnonymousRunsPerDay);
|
||||
var callsTable = new RecordTable(config.maxAnonymousCallsPerDay);
|
||||
|
||||
module.exports = apiLimitsMiddleware;
|
||||
@@ -1,42 +0,0 @@
|
||||
var config = require('../../../server_config/settings.json');
|
||||
|
||||
var debug = require('debug')('authMiddleware');
|
||||
|
||||
|
||||
var authMiddleware = function(req, res, next) {
|
||||
'use strict';
|
||||
|
||||
if (req.path.indexOf('/api/') === 0) {
|
||||
|
||||
|
||||
if (req.headers && req.headers['x-api-key']) {
|
||||
|
||||
// Test if it's an authorized key
|
||||
if (isApiKeyValid(req.headers['x-api-key'])) {
|
||||
|
||||
// Come in!
|
||||
debug('Authorized key: %s', req.headers['x-api-key']);
|
||||
res.locals.hasApiKey = true;
|
||||
|
||||
} else {
|
||||
|
||||
// Sorry :/
|
||||
debug('Unauthorized key %s', req.headers['x-api-key']);
|
||||
res.status(401).send('Unauthorized');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
debug('No authorization key');
|
||||
// It's ok for the moment but you might be blocked by the apiLimitsMiddleware, dude
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
function isApiKeyValid(apiKey) {
|
||||
return (config.authorizedKeys[apiKey]) ? true : false;
|
||||
}
|
||||
|
||||
module.exports = authMiddleware;
|
||||
@@ -1,12 +0,0 @@
|
||||
var wwwRedirectMiddleware = function(req, res, next) {
|
||||
'use strict';
|
||||
|
||||
// Redirect www.yellowlab.tools to yellowlab.tools without "www" (for SEO purpose)
|
||||
if(/^www\.yellowlab\.tools/.test(req.headers.host)) {
|
||||
res.redirect(301, req.protocol + '://' + req.headers.host.replace(/^www\./, '') + req.url);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = wwwRedirectMiddleware;
|
||||
@@ -5,7 +5,7 @@ var parse = require('parse-color');
|
||||
|
||||
var colorDiff = function() {
|
||||
|
||||
var COLOR_DIFF_THRESHOLD = 0.9;
|
||||
var COLOR_DIFF_THRESHOLD = 0.7;
|
||||
|
||||
this.compareAllColors = function(data) {
|
||||
debug('Starting to compare all colors...');
|
||||
@@ -55,7 +55,7 @@ var colorDiff = function() {
|
||||
};
|
||||
|
||||
this.parseOffender = function(offender) {
|
||||
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
|
||||
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender.value.message);
|
||||
return regexResult ? regexResult[1] : null;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
var debug = require('debug')('ylt:domAccessAgregator');
|
||||
|
||||
var domAccessAgregator = function() {
|
||||
'use strict';
|
||||
|
||||
this.agregate = function(data) {
|
||||
debug('Starting to agregate DOM Accesses...');
|
||||
|
||||
let count = 0;
|
||||
let offenders = {
|
||||
byType: {}
|
||||
};
|
||||
|
||||
const metricsToGather = [
|
||||
'DOMqueriesById',
|
||||
'DOMqueriesByTagName',
|
||||
'DOMqueriesByClassName',
|
||||
'DOMqueriesByQuerySelectorAll',
|
||||
'DOMinserts',
|
||||
'DOMmutationsInserts',
|
||||
'DOMmutationsRemoves',
|
||||
'DOMmutationsAttributes',
|
||||
'eventsBound'
|
||||
];
|
||||
|
||||
metricsToGather.forEach(key => {
|
||||
|
||||
if (data.toolsResults.phantomas.metrics[key]) {
|
||||
count += data.toolsResults.phantomas.metrics[key];
|
||||
}
|
||||
|
||||
offenders.byType[key] = [];
|
||||
if (data.toolsResults.phantomas.offenders[key]) {
|
||||
offenders.byType[key] = data.toolsResults.phantomas.offenders[key];
|
||||
}
|
||||
});
|
||||
|
||||
data.toolsResults.domAccessAgregator = {
|
||||
metrics: {
|
||||
DOMaccesses: count
|
||||
},
|
||||
offenders: {
|
||||
DOMaccesses: offenders
|
||||
}
|
||||
};
|
||||
|
||||
debug('Done agregating DOM Accesses.');
|
||||
|
||||
return data;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new domAccessAgregator();
|
||||
@@ -1,105 +0,0 @@
|
||||
var debug = require('debug')('ylt:isHttp2');
|
||||
var url = require('url');
|
||||
var Q = require('q');
|
||||
var http2 = require('is-http2');
|
||||
|
||||
var isHttp2 = function() {
|
||||
'use strict';
|
||||
|
||||
this.check = function(data) {
|
||||
debug('Starting to check for HTTP2 support...');
|
||||
|
||||
return this.checkHttp2(data)
|
||||
|
||||
.then(function(result) {
|
||||
|
||||
if (result.isHttp) {
|
||||
debug('The website is not even in HTTPS');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: false
|
||||
}
|
||||
};
|
||||
|
||||
} else if (result.isHttp2) {
|
||||
debug('HTTP/2 (or SPDY) is supported');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: true
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
debug('HTTP/2 is not supported');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add the supported protocols as offenders
|
||||
if (result.supportedProtocols) {
|
||||
debug('Supported protocols: ' + result.supportedProtocols.join(' '));
|
||||
data.toolsResults.http2.offenders = {
|
||||
http2: result.supportedProtocols
|
||||
};
|
||||
}
|
||||
|
||||
debug('End of HTTP2 support check');
|
||||
|
||||
return data;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
this.getParsedUrl = function(data) {
|
||||
return url.parse(data.toolsResults.phantomas.url);
|
||||
};
|
||||
|
||||
this.getProtocol = function(data) {
|
||||
return this.getParsedUrl(data).protocol;
|
||||
};
|
||||
|
||||
this.getDomain = function(data) {
|
||||
return this.getParsedUrl(data).hostname;
|
||||
};
|
||||
|
||||
this.checkHttp2 = function(data) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
// Check if it's HTTPS first
|
||||
if (this.getProtocol(data) === 'http:') {
|
||||
|
||||
deferred.resolve({
|
||||
isHttp: true
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path.
|
||||
http2(this.getDomain(data), {includeSpdy: true})
|
||||
|
||||
.then(function(result) {
|
||||
deferred.resolve(result);
|
||||
})
|
||||
|
||||
.catch(function(error) {
|
||||
debug('Error while checking HTTP2 support:');
|
||||
debug(error);
|
||||
deferred.reject('Error while checking for HTTP2 support');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new isHttp2();
|
||||
@@ -1,228 +0,0 @@
|
||||
var debug = require('debug')('ylt:jsExecutionTransformer');
|
||||
|
||||
var offendersHelpers = require('../offendersHelpers');
|
||||
var Collection = require('./phantomas/custom_modules/util/collection');
|
||||
|
||||
var jsExecutionTransformer = function() {
|
||||
|
||||
this.transform = function(data) {
|
||||
var javascriptExecutionTree = {};
|
||||
var jQueryFunctionsCollection = new Collection();
|
||||
|
||||
var metrics = {
|
||||
domInteractive: 0,
|
||||
domContentLoaded: 0,
|
||||
domContentLoadedEnd: 0,
|
||||
domComplete: 0,
|
||||
|
||||
DOMaccesses: 0,
|
||||
DOMaccessesOnScroll: 0,
|
||||
queriesWithoutResults: 0
|
||||
};
|
||||
|
||||
var offenders = {};
|
||||
|
||||
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
|
||||
if (hasjQuery) {
|
||||
metrics.jQueryCalls = 0;
|
||||
metrics.jQueryFunctionsUsed = 0;
|
||||
metrics.jQueryCallsOnEmptyObject = 0;
|
||||
metrics.jQueryNotDelegatedEvents = 0;
|
||||
|
||||
offenders.jQueryFunctionsUsed = [];
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
debug('Starting JS execution transformation');
|
||||
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
|
||||
|
||||
if (javascriptExecutionTree.children) {
|
||||
javascriptExecutionTree.children.forEach(function(node, index) {
|
||||
|
||||
var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
|
||||
|
||||
if (isABindWithoutEventDelegation(node, contextLength)) {
|
||||
metrics.jQueryNotDelegatedEvents += contextLength;
|
||||
node.warning = true;
|
||||
node.eventNotDelegated = true;
|
||||
}
|
||||
|
||||
if (node.data.resultsNumber === 0) {
|
||||
metrics.queriesWithoutResults ++;
|
||||
node.queryWithoutResults = true;
|
||||
node.warning = true;
|
||||
}
|
||||
|
||||
if (contextLength === 0) {
|
||||
metrics.jQueryCallsOnEmptyObject ++;
|
||||
node.jQueryCallOnEmptyObject = true;
|
||||
node.warning = true;
|
||||
}
|
||||
|
||||
if (node.data.type.indexOf('jQuery - ') === 0) {
|
||||
metrics.jQueryCalls ++;
|
||||
jQueryFunctionsCollection.push(node.data.type);
|
||||
}
|
||||
|
||||
// Mark errors with an error flag
|
||||
if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
|
||||
node.error = true;
|
||||
}
|
||||
|
||||
// Mark a performance flag
|
||||
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
|
||||
node.windowPerformance = true;
|
||||
|
||||
// Adjust the navigation timings (cause their not very well synchronised)
|
||||
switch(node.data.type) {
|
||||
case 'domInteractive':
|
||||
javascriptExecutionTree.data.domInteractive = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoaded':
|
||||
javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoadedEnd':
|
||||
javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
|
||||
break;
|
||||
case 'domComplete':
|
||||
javascriptExecutionTree.data.domComplete = node.data.timestamp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fix rare bug when domComplete was never triggered
|
||||
if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
|
||||
javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
|
||||
}
|
||||
|
||||
// Transform domPaths into objects
|
||||
changeListOfDomPaths(node);
|
||||
|
||||
// Count the number of DOM accesses, by counting the tree leafs
|
||||
metrics.DOMaccesses += countTreeLeafs(node);
|
||||
});
|
||||
|
||||
// Count the number of different jQuery functions called
|
||||
if (hasjQuery) {
|
||||
|
||||
jQueryFunctionsCollection.sort().forEach(function(fnName, cnt) {
|
||||
if (fnName === 'jQuery - find') {
|
||||
fnName = 'jQuery - $';
|
||||
}
|
||||
metrics.jQueryFunctionsUsed ++;
|
||||
offenders.jQueryFunctionsUsed.push({
|
||||
functionName: fnName.substring(9),
|
||||
count: cnt
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
debug('JS execution transformation complete');
|
||||
|
||||
|
||||
if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
|
||||
debug('Starting scroll execution transformation');
|
||||
offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
|
||||
if (offenders.DOMaccessesOnScroll.children) {
|
||||
offenders.DOMaccessesOnScroll.children.forEach(function(node) {
|
||||
|
||||
// Mark a event flag
|
||||
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
|
||||
node.windowPerformance = true;
|
||||
}
|
||||
|
||||
// Transform domPaths into objects
|
||||
changeListOfDomPaths(node);
|
||||
|
||||
// Count the number of DOM accesses, by counting the tree leafs
|
||||
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
|
||||
});
|
||||
}
|
||||
debug('Scroll execution transformation complete');
|
||||
} else {
|
||||
debug('Could not parse scrollExecutionTree');
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
data.javascriptExecutionTree = javascriptExecutionTree;
|
||||
|
||||
data.toolsResults.jsExecutionTransformer = {
|
||||
metrics: metrics,
|
||||
offenders: offenders
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
function treeRecursiveParser(node, fn) {
|
||||
if (node.children) {
|
||||
node.children.forEach(function(child) {
|
||||
treeRecursiveParser(child, fn);
|
||||
});
|
||||
}
|
||||
fn(node);
|
||||
}
|
||||
|
||||
function changeListOfDomPaths(rootNode) {
|
||||
treeRecursiveParser(rootNode, function(node) {
|
||||
|
||||
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
|
||||
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
|
||||
}
|
||||
|
||||
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
|
||||
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
|
||||
}
|
||||
|
||||
if (node.data.type === 'insertBefore') {
|
||||
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Returns the number of leafs (nodes without children)
|
||||
function countTreeLeafs(rootNode) {
|
||||
var count = 0;
|
||||
|
||||
treeRecursiveParser(rootNode, function(node) {
|
||||
if (!node.children &&
|
||||
!node.error &&
|
||||
!node.windowPerformance &&
|
||||
node.data.type !== 'jQuery loaded') {
|
||||
count ++;
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function isPureString(str) {
|
||||
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
|
||||
}
|
||||
|
||||
function isABindWithoutEventDelegation(node, contextLength) {
|
||||
// Count only on larger bindings
|
||||
if (contextLength <= 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
|
||||
var child = node.children[0];
|
||||
if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = new jsExecutionTransformer();
|
||||
@@ -1,251 +0,0 @@
|
||||
/**
|
||||
* Overwritting the original spying functions in scope.js
|
||||
* This is done so we now have a before AND an after callback on the spy
|
||||
*
|
||||
* @see http://code.jquery.com/jquery-1.10.2.js
|
||||
* @see http://code.jquery.com/jquery-2.0.3.js
|
||||
*/
|
||||
/* global document: true, window: true */
|
||||
|
||||
exports.version = '0.2';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
var responseEndTime = Date.now();
|
||||
|
||||
phantomas.on('responseEnd', function() {
|
||||
responseEndTime = Date.now();
|
||||
});
|
||||
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(responseEndTime) {
|
||||
(function(phantomas) {
|
||||
|
||||
// Overwritting phantomas spy function
|
||||
(function() {
|
||||
var enabled = true;
|
||||
|
||||
// turn off spying to not include internal phantomas actions
|
||||
function spyEnabled(state, reason) {
|
||||
enabled = (state === true);
|
||||
|
||||
phantomas.log('Spying ' + (enabled ? 'enabled' : 'disabled') + (reason ? ' - ' + reason : ''));
|
||||
}
|
||||
|
||||
phantomas.log('Overwritting phantomas spy function');
|
||||
|
||||
function spy(obj, fn, callbackBefore, callbackAfter) {
|
||||
var origFn = obj && obj[fn];
|
||||
|
||||
if (typeof origFn !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
phantomas.log('Attaching a YLT spy to "' + fn + '" function...');
|
||||
|
||||
obj[fn] = function() {
|
||||
var result;
|
||||
var err;
|
||||
|
||||
// Before
|
||||
if (enabled && callbackBefore) {
|
||||
callbackBefore.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Execute
|
||||
try {
|
||||
|
||||
result = origFn.apply(this, arguments);
|
||||
|
||||
} catch(e) {
|
||||
|
||||
// Catching the err for the moment, because we need to make sure the callbackAfter function is called.
|
||||
|
||||
phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
|
||||
phantomas.log(arguments);
|
||||
|
||||
err = e;
|
||||
|
||||
} finally {
|
||||
|
||||
// After
|
||||
if (enabled && callbackAfter) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
callbackAfter.apply(this, [result].concat(args));
|
||||
}
|
||||
|
||||
if (err) {
|
||||
phantomas.log('Re-throwing the error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// copy custom properties of original function to the mocked one
|
||||
Object.keys(origFn).forEach(function(key) {
|
||||
obj[fn][key] = origFn[key];
|
||||
});
|
||||
|
||||
obj[fn].prototype = origFn.prototype;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
phantomas.spyEnabled = spyEnabled;
|
||||
phantomas.spy = spy;
|
||||
})();
|
||||
|
||||
|
||||
|
||||
// Adding some code for the Javascript execution tree construction
|
||||
(function() {
|
||||
|
||||
var root = new ContextTreeNode(null, {type: 'main'});
|
||||
var currentContext = root;
|
||||
var depth = 0;
|
||||
|
||||
|
||||
// Add a child but don't enter its context
|
||||
function pushContext(data) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 0) {
|
||||
data.timestamp = Date.now() - responseEndTime;
|
||||
data.loadingStep = phantomas.currentStep || '';
|
||||
}
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 0) {
|
||||
if (data.backtrace) {
|
||||
delete data.backtrace;
|
||||
}
|
||||
if (data.resultsNumber) {
|
||||
delete data.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
currentContext.addChild(data);
|
||||
}
|
||||
|
||||
// Add a child to the current context and enter its context
|
||||
function enterContext(data) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 0) {
|
||||
data.timestamp = Date.now() - responseEndTime;
|
||||
data.loadingStep = phantomas.currentStep || '';
|
||||
}
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 0) {
|
||||
if (data.backtrace) {
|
||||
delete data.backtrace;
|
||||
}
|
||||
if (data.resultsNumber) {
|
||||
delete data.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
currentContext = currentContext.addChild(data);
|
||||
|
||||
depth ++;
|
||||
}
|
||||
|
||||
// Save given data in the current context and jump change current context to its parent
|
||||
function leaveContext(moreData) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 1) {
|
||||
currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
|
||||
}
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 1) {
|
||||
if (moreData && moreData.backtrace) {
|
||||
delete moreData.backtrace;
|
||||
}
|
||||
if (moreData && moreData.resultsNumber) {
|
||||
delete moreData.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge previous data with moreData (ovewrites if exists)
|
||||
if (moreData) {
|
||||
for (var key in moreData) {
|
||||
currentContext.data[key] = moreData[key];
|
||||
}
|
||||
}
|
||||
|
||||
var parent = currentContext.parent;
|
||||
if (parent === null) {
|
||||
console.error('Error: trying to close root context in ContextTree');
|
||||
} else {
|
||||
currentContext = parent;
|
||||
}
|
||||
|
||||
depth --;
|
||||
}
|
||||
|
||||
function getContextData() {
|
||||
return currentContext.data;
|
||||
}
|
||||
|
||||
// Returns a clean object, without the parent which causes recursive loops
|
||||
function readFullTree() {
|
||||
// Return null if the contextTree is not correctly closed
|
||||
if (root !== currentContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function recusiveRead(node) {
|
||||
if (node.children.length === 0) {
|
||||
delete node.children;
|
||||
} else {
|
||||
for (var i=0, max=node.children.length ; i<max ; i++) {
|
||||
recusiveRead(node.children[i]);
|
||||
}
|
||||
}
|
||||
delete node.parent;
|
||||
}
|
||||
recusiveRead(root);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// Empty the tree
|
||||
function resetTree() {
|
||||
root = new ContextTreeNode(null, {type: 'main'});
|
||||
currentContext = root;
|
||||
depth = 0;
|
||||
}
|
||||
|
||||
|
||||
function ContextTreeNode(parent, data) {
|
||||
this.data = data;
|
||||
this.parent = parent;
|
||||
this.children = [];
|
||||
|
||||
this.addChild = function(data) {
|
||||
var child = new ContextTreeNode(this, data);
|
||||
this.children.push(child);
|
||||
return child;
|
||||
};
|
||||
}
|
||||
|
||||
phantomas.log('Adding some contextTree functions to phantomas');
|
||||
phantomas.pushContext = pushContext;
|
||||
phantomas.enterContext = enterContext;
|
||||
phantomas.leaveContext = leaveContext;
|
||||
phantomas.getContextData = getContextData;
|
||||
phantomas.readFullTree = readFullTree;
|
||||
phantomas.resetTree = resetTree;
|
||||
|
||||
})();
|
||||
|
||||
})(window.__phantomas);
|
||||
}, responseEndTime);
|
||||
});
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Analyzes AJAX requests
|
||||
*/
|
||||
/* global window: true */
|
||||
|
||||
exports.version = '0.2.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('ajaxRequests'); // @desc number of AJAX requests
|
||||
phantomas.setMetric('synchronousXHR'); // @desc number of synchronous
|
||||
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
phantomas.spy(window.XMLHttpRequest.prototype, 'open', null, function(result, method, url, async) {
|
||||
phantomas.incrMetric('ajaxRequests');
|
||||
phantomas.addOffender('ajaxRequests', '<%s> [%s]', url, method);
|
||||
|
||||
if (async === false) {
|
||||
phantomas.incrMetric('synchronousXHR');
|
||||
phantomas.addOffender('synchronousXHR', url);
|
||||
phantomas.log('ajaxRequests: synchronous XMLHttpRequest call to <%s>', url);
|
||||
}
|
||||
}, true);
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Lists the number of different characters
|
||||
*/
|
||||
/* global document: true, Node: true, window: true */
|
||||
|
||||
exports.version = '1.0';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
//phantomas.setMetric('differentCharacters'); // @desc the number of different characters in the body @offenders
|
||||
|
||||
phantomas.on('report', function() {
|
||||
phantomas.log("charactersCount: starting to analyze characters on page...");
|
||||
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
|
||||
phantomas.spyEnabled(false, 'analyzing characters on page');
|
||||
|
||||
function getLetterCount(arr){
|
||||
return arr.reduce(function(prev, next){
|
||||
prev[next] = 1;
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (document.body && document.body.textContent) {
|
||||
var allText = '';
|
||||
|
||||
// Traverse all DOM Tree to read text
|
||||
var runner = new phantomas.nodeRunner();
|
||||
runner.walk(document.body, function(node, depth) {
|
||||
switch (node.nodeType) {
|
||||
|
||||
// Grabing text nodes
|
||||
case Node.TEXT_NODE:
|
||||
if (node.parentNode.tagName !== 'SCRIPT' && node.parentNode.tagName !== 'STYLE') {
|
||||
allText += node.nodeValue;
|
||||
}
|
||||
break;
|
||||
|
||||
// Grabing CSS content properties
|
||||
case Node.ELEMENT_NODE:
|
||||
if (node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE') {
|
||||
allText += window.getComputedStyle(node).getPropertyValue('content');
|
||||
allText += window.getComputedStyle(node, ':before').getPropertyValue('content');
|
||||
allText += window.getComputedStyle(node, ':after').getPropertyValue('content');
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Reduce all text found in page into a string of unique chars
|
||||
var charactersList = getLetterCount(allText.split(''));
|
||||
var charsetString = Object.keys(charactersList).sort().join('');
|
||||
|
||||
// Remove blank characters
|
||||
charsetString = charsetString.replace(/\s/g, '');
|
||||
|
||||
phantomas.setMetric('differentCharacters', charsetString.length);
|
||||
phantomas.addOffender('differentCharacters', charsetString);
|
||||
}
|
||||
|
||||
phantomas.spyEnabled(true);
|
||||
}(window.__phantomas));
|
||||
});
|
||||
|
||||
phantomas.log("charactersCount: analyzing characters done.");
|
||||
});
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Analyzes DOM hidden content
|
||||
*/
|
||||
/* global document: true, Node: true, window: true */
|
||||
|
||||
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) {
|
||||
var runner = new phantomas.nodeRunner(),
|
||||
lazyLoadableImages = {};
|
||||
|
||||
phantomas.spyEnabled(false, 'analyzing hidden content');
|
||||
|
||||
runner.walk(document.body, function(node, depth) {
|
||||
switch (node.nodeType) {
|
||||
case Node.ELEMENT_NODE:
|
||||
// @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle
|
||||
var styles = window.getComputedStyle(node);
|
||||
|
||||
if (styles && styles.getPropertyValue('display') === 'none') {
|
||||
if (typeof node.innerHTML === 'string') {
|
||||
var size = node.innerHTML.length;
|
||||
phantomas.incrMetric('hiddenContentSize', size);
|
||||
|
||||
// log hidden containers bigger than 1 kB
|
||||
if (size > 1024) {
|
||||
phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (images[i].width === 1 && images[i].height === 1) 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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,387 +0,0 @@
|
||||
/**
|
||||
* Analyzes DOM queries done via native DOM methods
|
||||
*/
|
||||
/* global Element: true, Document: true, Node: true, window: true */
|
||||
|
||||
exports.version = '0.10.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
|
||||
phantomas.setMetric('DOMqueriesWithoutResults'); // @desc number of DOM queries that retutned nothing @offenders
|
||||
phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
|
||||
phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
|
||||
phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
|
||||
phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls
|
||||
phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts
|
||||
phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of DOM queries called more than once
|
||||
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
|
||||
|
||||
// fake native DOM functions
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
function querySpy(type, query, fnName, context, hasNoResults) {
|
||||
phantomas.emit('domQuery', type, query, fnName, context, hasNoResults); // @desc DOM query has been made
|
||||
}
|
||||
|
||||
phantomas.spy(Document.prototype, 'getElementById', function(id) {
|
||||
phantomas.incrMetric('DOMqueriesById');
|
||||
phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'getElementById',
|
||||
callDetails: {
|
||||
arguments: ['#' + id]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result, args) {
|
||||
var id = args[0];
|
||||
|
||||
querySpy('id', '#' + id, 'getElementById', '#document', (result === null));
|
||||
|
||||
var moreData = {
|
||||
resultsNumber : (result === null) ? 0 : 1
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
});
|
||||
|
||||
// selectors by class name
|
||||
function selectorClassNameSpyBefore(className) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
|
||||
phantomas.incrMetric('DOMqueriesByClassName');
|
||||
phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'getElementsByClassName',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: ['.' + className]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function selectorClassNameAfter(result, args) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var className = args[0];
|
||||
var context = phantomas.getDOMPath(this);
|
||||
|
||||
querySpy('class', '.' + className, 'getElementsByClassName', context, (result.length === 0));
|
||||
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length > 0) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
|
||||
phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
|
||||
phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
|
||||
|
||||
// selectors by tag name
|
||||
function selectorTagNameSpyBefore(tagName) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
|
||||
phantomas.incrMetric('DOMqueriesByTagName');
|
||||
phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'getElementsByTagName',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [tagName]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function selectorTagNameSpyAfter(result, args) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var tagName = args[0];
|
||||
var context = phantomas.getDOMPath(this);
|
||||
|
||||
querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context, (result.length === 0));
|
||||
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length > 0) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
|
||||
phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
|
||||
phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
|
||||
|
||||
|
||||
// selector queries
|
||||
function selectorQuerySpy(selector, context) {
|
||||
phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
|
||||
phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context);
|
||||
}
|
||||
|
||||
function selectorQuerySpyBefore(selector) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
selectorQuerySpy(selector, context);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'querySelector',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function selectorQuerySpyAfter(result, args) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var selector = args[0];
|
||||
var context = phantomas.getDOMPath(this);
|
||||
|
||||
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
|
||||
|
||||
var moreData = {
|
||||
resultsNumber : result ? 1 : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
|
||||
function selectorAllQuerySpyBefore(selector) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
selectorQuerySpy(selector, context);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'querySelectorAll',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function selectorAllQuerySpryAfter(result, args) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var selector = args[0];
|
||||
var context = phantomas.getDOMPath(this);
|
||||
|
||||
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
|
||||
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length > 0) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
|
||||
phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
|
||||
phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
|
||||
phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
|
||||
phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
|
||||
|
||||
|
||||
// count DOM inserts
|
||||
function appendChild(child, element, context, appended) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
// ignore appending to the node that's not yet added to DOM tree
|
||||
if (!element.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't count elements added to fragments as a DOM inserts (issue #350)
|
||||
// DocumentFragment > div[0]
|
||||
if (context.indexOf('DocumentFragment') === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
phantomas.incrMetric('DOMinserts');
|
||||
phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appended, context);
|
||||
|
||||
//phantomas.log('DOM insert: node "%s" appended to "%s"', appended, context);
|
||||
}
|
||||
|
||||
function appendChildSpyBefore(child) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
var appended = phantomas.getDOMPath(child);
|
||||
appendChild(child, this, context, appended);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'appendChild',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [appended]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function insertBeforeSpyBefore(child, refElement) {
|
||||
/*jshint validthis: true */
|
||||
|
||||
var context = phantomas.getDOMPath(this);
|
||||
var appended = phantomas.getDOMPath(child);
|
||||
var referent = phantomas.getDOMPath(refElement);
|
||||
appendChild(child, this, context, appended);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'insertBefore',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [
|
||||
appended,
|
||||
referent
|
||||
]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, function(result) {
|
||||
phantomas.leaveContext();
|
||||
});
|
||||
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
|
||||
phantomas.leaveContext();
|
||||
});
|
||||
|
||||
|
||||
phantomas.spy(Document.prototype, 'createElement', function(tagName) {
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'createElement',
|
||||
callDetails: {
|
||||
arguments: [tagName]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result, args) {
|
||||
phantomas.leaveContext();
|
||||
});
|
||||
|
||||
|
||||
phantomas.spy(Document.prototype, 'createTextNode', function(text) {
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'createTextNode',
|
||||
callDetails: {
|
||||
arguments: [text]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result, args) {
|
||||
phantomas.leaveContext();
|
||||
});
|
||||
|
||||
|
||||
phantomas.spy(Document.prototype, 'createDocumentFragment', function() {
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'createDocumentFragment',
|
||||
callDetails: {
|
||||
arguments: []
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result, args) {
|
||||
phantomas.leaveContext();
|
||||
});
|
||||
|
||||
|
||||
phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) {
|
||||
var target = phantomas.getDOMPath(element);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'getComputedStyle',
|
||||
callDetails: {
|
||||
arguments: [target, pseudoElement]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result, args) {
|
||||
phantomas.leaveContext();
|
||||
});
|
||||
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
|
||||
// report DOM queries that return no results (issue #420)
|
||||
phantomas.on('domQuery', function(type, query, fnName, context, hasNoResults) {
|
||||
// ignore DOM queries within DOM fragments (used internally by jQuery)
|
||||
if (context.indexOf('body') !== 0 && context.indexOf('#document') !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasNoResults === true) {
|
||||
phantomas.incrMetric('DOMqueriesWithoutResults');
|
||||
phantomas.addOffender('DOMqueriesWithoutResults', '%s (in %s) using %s', query, context, fnName);
|
||||
}
|
||||
});
|
||||
|
||||
// count DOM queries by either ID, tag name, class name and selector query
|
||||
// @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
|
||||
var Collection = require('../../util/collection'),
|
||||
DOMqueries = new Collection();
|
||||
|
||||
phantomas.on('domQuery', function(type, query, fnName, context) {
|
||||
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
|
||||
phantomas.incrMetric('DOMqueries');
|
||||
|
||||
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 + ')');
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.on('report', function() {
|
||||
DOMqueries.sort().forEach(function(query, cnt) {
|
||||
if (cnt > 1) {
|
||||
phantomas.incrMetric('DOMqueriesDuplicated');
|
||||
phantomas.incrMetric('DOMqueriesAvoidable', cnt - 1);
|
||||
phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* Analyzes events bound to DOM elements
|
||||
*/
|
||||
/* global Document: true, Element: true, window: true */
|
||||
|
||||
exports.version = '0.4.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
|
||||
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
|
||||
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
|
||||
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
// spy calls to EventTarget.addEventListener
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
|
||||
function eventSpyBefore(eventType) {
|
||||
/* jshint validthis: true */
|
||||
var path = phantomas.getDOMPath(this);
|
||||
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
|
||||
|
||||
phantomas.incrMetric('eventsBound');
|
||||
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
|
||||
phantomas.log('event ' + eventType + ' bound');
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'addEventListener',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [path]
|
||||
},
|
||||
arguments: [eventType]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
// count window.addEventListener('scroll', ...) - issue #508
|
||||
if (eventType === 'scroll' && (path === 'window' || path === '#document')) {
|
||||
phantomas.incrMetric('eventsScrollBound');
|
||||
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', phantomas.getBacktrace(), path);
|
||||
}
|
||||
}
|
||||
|
||||
function eventSpyAfter(result) {
|
||||
phantomas.leaveContext();
|
||||
}
|
||||
|
||||
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
|
||||
// spy calls to EventTarget.dispatchEvent
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent
|
||||
phantomas.spy(Element.prototype, 'dispatchEvent', function(ev) {
|
||||
/* jshint validthis: true */
|
||||
var path = phantomas.getDOMPath(this);
|
||||
|
||||
phantomas.log('Core JS event: triggered "%s" on "%s"', ev.type, path);
|
||||
|
||||
phantomas.incrMetric('eventsDispatched');
|
||||
phantomas.addOffender('eventsDispatched', '"%s" on "%s"', ev.type, path);
|
||||
});
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
|
||||
phantomas.on('report', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
// Check if a window.onscroll function is defined
|
||||
if (typeof(window.onscroll) === "function") {
|
||||
phantomas.incrMetric('eventsScrollBound');
|
||||
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', '', 'window.onscroll');
|
||||
}
|
||||
}(window.__phantomas));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,381 +0,0 @@
|
||||
/**
|
||||
* Analyzes jQuery activity
|
||||
*
|
||||
* @see http://code.jquery.com/jquery-1.10.2.js
|
||||
* @see http://code.jquery.com/jquery-2.0.3.js
|
||||
*/
|
||||
/* global document: true, window: true */
|
||||
/* jshint -W030 */
|
||||
|
||||
exports.version = '1.0.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
|
||||
phantomas.setMetric('jQueryVersionsLoaded'); // @desc number of loaded jQuery "instances" (even in the same version)
|
||||
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
|
||||
phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event
|
||||
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
|
||||
phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
|
||||
|
||||
var jQueryFunctions = [
|
||||
// DOM manipulations
|
||||
'html',
|
||||
'append',
|
||||
'appendTo',
|
||||
'prepend',
|
||||
'prependTo',
|
||||
'before',
|
||||
'insertBefore',
|
||||
'after',
|
||||
'insertAfter',
|
||||
'remove',
|
||||
'detach',
|
||||
'empty',
|
||||
'clone',
|
||||
'replaceWith',
|
||||
'replaceAll',
|
||||
'text',
|
||||
'wrap',
|
||||
'wrapAll',
|
||||
'wrapInner',
|
||||
'unwrap',
|
||||
|
||||
// Style manipulations
|
||||
'css',
|
||||
'offset',
|
||||
'position',
|
||||
'height',
|
||||
'innerHeight',
|
||||
'outerHeight',
|
||||
'width',
|
||||
'innerWidth',
|
||||
'outerWidth',
|
||||
'scrollLeft',
|
||||
'scrollTop',
|
||||
|
||||
// Animations
|
||||
'hide',
|
||||
'show',
|
||||
'toggle',
|
||||
'animate',
|
||||
'fadeIn',
|
||||
'fadeOut',
|
||||
'fadeTo',
|
||||
'fadeToggle',
|
||||
'slideDown',
|
||||
'slideUp',
|
||||
'slideToggle',
|
||||
|
||||
// Generic events
|
||||
'on',
|
||||
'off',
|
||||
'live',
|
||||
'die',
|
||||
'delegate',
|
||||
'undelegate',
|
||||
'one',
|
||||
'bind',
|
||||
'unbind',
|
||||
|
||||
// More events
|
||||
'blur',
|
||||
'change',
|
||||
'click',
|
||||
'dblclick',
|
||||
'error',
|
||||
'focus',
|
||||
'focusin',
|
||||
'focusout',
|
||||
'hover',
|
||||
'keydown',
|
||||
'keypress',
|
||||
'keyup',
|
||||
'load',
|
||||
'mousedown',
|
||||
'mouseenter',
|
||||
'mouseleave',
|
||||
'mousemove',
|
||||
'mouseout',
|
||||
'mouseover',
|
||||
'mouseup',
|
||||
'resize',
|
||||
'scroll',
|
||||
'select',
|
||||
'submit',
|
||||
'unload',
|
||||
|
||||
// Attributes
|
||||
'attr',
|
||||
'prop',
|
||||
'removeAttr',
|
||||
'removeProp',
|
||||
'val',
|
||||
'hasClass',
|
||||
'addClass',
|
||||
'removeClass',
|
||||
'toggleClass',
|
||||
];
|
||||
|
||||
var jQueryTraversalFunctions = [
|
||||
'children',
|
||||
'closest',
|
||||
'find',
|
||||
'next',
|
||||
'nextAll',
|
||||
'nextUntil',
|
||||
'offsetParent',
|
||||
'parent',
|
||||
'parents',
|
||||
'parentsUntil',
|
||||
'prev',
|
||||
'prevAll',
|
||||
'prevUntil',
|
||||
'siblings'
|
||||
];
|
||||
|
||||
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
|
||||
|
||||
// spy calls to jQuery functions
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
|
||||
(function(phantomas) {
|
||||
var oldJQuery;
|
||||
|
||||
phantomas.spyGlobalVar('jQuery', function(jQuery) {
|
||||
var version;
|
||||
|
||||
if (!jQuery || !jQuery.fn) {
|
||||
phantomas.log('jQuery: unable to detect version!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Tag the current version of jQuery to avoid multiple reports of jQuery being loaded
|
||||
// when it's actually only restored via $.noConflict(true) - see comments in #435
|
||||
if (jQuery.__phantomas === true) {
|
||||
phantomas.log('jQuery: this instance has already been seen by phantomas');
|
||||
return;
|
||||
}
|
||||
jQuery.__phantomas = true;
|
||||
|
||||
// report the version of jQuery
|
||||
version = jQuery.fn.jquery;
|
||||
phantomas.emit('jQueryLoaded', version);
|
||||
|
||||
phantomas.pushContext({
|
||||
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
|
||||
callDetails: {
|
||||
arguments: ['version ' + version]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
oldJQuery = version;
|
||||
|
||||
// jQuery.ready.promise
|
||||
// works for jQuery 1.8.0+ (released Aug 09 2012)
|
||||
phantomas.spy(jQuery.ready, 'promise', function(func) {
|
||||
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
|
||||
phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3));
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'jQuery - onDOMReady',
|
||||
callDetails: {
|
||||
arguments: [func]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!');
|
||||
|
||||
|
||||
// Sizzle calls - jQuery.find
|
||||
// works for jQuery 1.3+ (released Jan 13 2009)
|
||||
phantomas.spy(jQuery, 'find', function(selector, context) {
|
||||
phantomas.incrMetric('jQuerySizzleCalls');
|
||||
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'jQuery - Sizzle call',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [phantomas.getDOMPath(context)]
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result) {
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
|
||||
|
||||
|
||||
phantomas.spy(jQuery.fn, 'init', function(selector, context) {
|
||||
if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) {
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'jQuery - find',
|
||||
callDetails: {
|
||||
arguments: [selector]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}, function(result) {
|
||||
var data = phantomas.getContextData();
|
||||
|
||||
if (data.type === 'jQuery - find' &&
|
||||
!data.callDetails.context &&
|
||||
data.callDetails.arguments.length === 1 &&
|
||||
/^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) {
|
||||
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!jQuery.event) {
|
||||
phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
|
||||
var path = phantomas.getDOMPath(elem),
|
||||
type = ev.type || ev;
|
||||
|
||||
phantomas.log('Event: triggered "%s" on "%s"', type, path);
|
||||
|
||||
phantomas.incrMetric('jQueryEventTriggers');
|
||||
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
|
||||
});
|
||||
}
|
||||
|
||||
// jQuery events bound to window' onLoad event (#451)
|
||||
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
|
||||
if ((eventName === 'load') && (this[0] === window)) {
|
||||
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
|
||||
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
|
||||
}
|
||||
});
|
||||
|
||||
// Add spys on many jQuery functions
|
||||
jQueryFunctions.forEach(function(functionName) {
|
||||
|
||||
phantomas.spy(jQuery.fn, functionName, function(args) {
|
||||
|
||||
// Clean args
|
||||
args = [].slice.call(arguments);
|
||||
args.forEach(function(arg, index) {
|
||||
|
||||
if (arg instanceof Array) {
|
||||
arg = '[Array]';
|
||||
}
|
||||
|
||||
if (arg instanceof Object) {
|
||||
|
||||
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
|
||||
|
||||
arg = phantomas.getDOMPath(arg[0]) || 'unknown';
|
||||
|
||||
} else if (arg instanceof HTMLElement) {
|
||||
|
||||
arg = phantomas.getDOMPath(arg) || 'unknown';
|
||||
|
||||
} else if (typeof arg === 'function') {
|
||||
|
||||
arg = '(function)';
|
||||
|
||||
} else {
|
||||
|
||||
try {
|
||||
|
||||
for (var key in arg) {
|
||||
if (typeof arg[key] === 'function') {
|
||||
arg[key] = '(function)';
|
||||
}
|
||||
}
|
||||
|
||||
arg = JSON.stringify(arg);
|
||||
} catch(e) {
|
||||
arg = '[Object]';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if ((typeof arg === 'string' || arg instanceof String) && arg.length > 200) {
|
||||
arg = arg.substring(0, 200) + '...';
|
||||
}
|
||||
|
||||
if (typeof arg === 'function') {
|
||||
arg = '(function)';
|
||||
}
|
||||
|
||||
if (arg === true) {
|
||||
arg = 'true';
|
||||
}
|
||||
|
||||
if (arg === false) {
|
||||
arg = 'false';
|
||||
}
|
||||
|
||||
if (arg === null) {
|
||||
arg = 'null';
|
||||
}
|
||||
|
||||
if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) {
|
||||
arg = 'undefined';
|
||||
}
|
||||
|
||||
args[index] = arg;
|
||||
});
|
||||
|
||||
var elements = [];
|
||||
for (var i = 0 ; i < this.length ; i++) {
|
||||
elements.push(phantomas.getDOMPath(this[i]));
|
||||
}
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'jQuery - ' + functionName,
|
||||
callDetails: {
|
||||
context: {
|
||||
length: this.length,
|
||||
elements: elements
|
||||
},
|
||||
arguments: args
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
}, function(result) {
|
||||
if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
|
||||
var moreData = {
|
||||
resultsNumber : (result && result.length) ? result.length : 0
|
||||
};
|
||||
phantomas.leaveContext(moreData);
|
||||
} else {
|
||||
phantomas.leaveContext();
|
||||
}
|
||||
}) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
|
||||
});
|
||||
});
|
||||
})(window.__phantomas);
|
||||
}, jQueryFunctions, jQueryTraversalFunctions);
|
||||
});
|
||||
|
||||
|
||||
phantomas.on('jQueryLoaded', function(version) {
|
||||
phantomas.log('jQuery: loaded v' + version);
|
||||
phantomas.setMetric('jQueryVersion', version);
|
||||
|
||||
// report multiple jQuery "instances" (issue #435)
|
||||
phantomas.incrMetric('jQueryVersionsLoaded');
|
||||
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
|
||||
});
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Reports the use of functions known to be serious performance bottlenecks in JS
|
||||
*
|
||||
* @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
|
||||
* @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
|
||||
* @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
|
||||
*
|
||||
* Run phantomas with --spy-eval to count eval() calls (see issue #467)
|
||||
*/
|
||||
/* global document: true, window: true */
|
||||
|
||||
exports.version = '0.2';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln @offenders
|
||||
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval) @offenders
|
||||
|
||||
// spy calls to eval only when requested (issue #467)
|
||||
var spyEval = phantomas.getParam('spy-eval') === true;
|
||||
if (!spyEval) {
|
||||
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
|
||||
}
|
||||
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(spyEval) {
|
||||
(function(phantomas) {
|
||||
function report(msg, caller, backtrace, metric) {
|
||||
phantomas.log(msg + ': from ' + caller + '!');
|
||||
phantomas.log('Backtrace: ' + backtrace);
|
||||
|
||||
phantomas.incrMetric(metric);
|
||||
phantomas.addOffender(metric, "%s from %s", msg, caller);
|
||||
}
|
||||
|
||||
// spy calls to eval()
|
||||
if (spyEval) {
|
||||
phantomas.spy(window, 'eval', function(code) {
|
||||
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
||||
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
|
||||
});
|
||||
}
|
||||
|
||||
// spy calls to setTimeout / setInterval with string passed instead of a function
|
||||
phantomas.spy(window, 'setTimeout', function(fn, interval) {
|
||||
if (typeof fn !== 'string') return;
|
||||
|
||||
report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
||||
});
|
||||
|
||||
phantomas.spy(window, 'setInterval', function(fn, interval) {
|
||||
if (typeof fn !== 'string') return;
|
||||
|
||||
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
||||
});
|
||||
|
||||
// spy document.write(ln)
|
||||
phantomas.spy(document, 'write', function(arg) {
|
||||
report('document.write() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
|
||||
});
|
||||
|
||||
phantomas.spy(document, 'writeln', function(arg) {
|
||||
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
|
||||
});
|
||||
})(window.__phantomas);
|
||||
}, spyEval);
|
||||
});
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
|
||||
*/
|
||||
|
||||
exports.version = '0.3.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('jsErrors'); // @desc number of JavaScript errors
|
||||
|
||||
function formatTrace(trace) {
|
||||
var ret = [];
|
||||
|
||||
if(Array.isArray(trace)) {
|
||||
trace.forEach(function(entry) {
|
||||
ret.push((entry.function ? entry.function + ' ' : '') + (entry.sourceURL || entry.file) + ':' + entry.line);
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
phantomas.on('jserror', function(msg, trace) {
|
||||
trace = formatTrace(trace);
|
||||
|
||||
phantomas.log(msg);
|
||||
phantomas.log('Backtrace: ' + trace.join(' / '));
|
||||
|
||||
phantomas.incrMetric('jsErrors');
|
||||
phantomas.addOffender('jsErrors', msg + ' - ' + trace.join(' / '));
|
||||
|
||||
// Yeah, this is weird, i'm sending the error back to the browser...
|
||||
phantomas.evaluate(function(msg, caller, trace) {
|
||||
(function(phantomas) {
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'error',
|
||||
callDetails: {
|
||||
arguments: [msg]
|
||||
},
|
||||
caller: caller,
|
||||
backtrace: trace
|
||||
});
|
||||
|
||||
})(window.__phantomas);
|
||||
}, msg, trace[0], trace.join(' / '));
|
||||
});
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Saves the javascript interractions with the DOM
|
||||
*
|
||||
* Run phantomas with --js-execution-tree option to use this module
|
||||
*/
|
||||
|
||||
exports.version = '0.1';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('javascriptExecutionTree');
|
||||
|
||||
// save data
|
||||
phantomas.on('report', function() {
|
||||
phantomas.log('JS execution tree: Reading execution tree JSON');
|
||||
|
||||
phantomas.evaluate(function() {(function(phantomas) {
|
||||
var fullTree = phantomas.readFullTree();
|
||||
|
||||
if (fullTree === null) {
|
||||
phantomas.log('JS execution tree: error, the execution tree is not correctly closed');
|
||||
return;
|
||||
}
|
||||
|
||||
phantomas.setMetric('javascriptExecutionTree', true, true);
|
||||
phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree));
|
||||
|
||||
})(window.__phantomas);});
|
||||
});
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* Analyzes images and detects which one can be lazy-loaded (are below the fold)
|
||||
*
|
||||
* @see https://github.com/macbre/phantomas/issues/494
|
||||
*/
|
||||
/* global document: true, window: true */
|
||||
|
||||
exports.version = '1.0.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('lazyLoadableImagesBelowTheFold'); // @desc number of images displayed below the fold that can be lazy-loaded
|
||||
|
||||
phantomas.on('report', function() {
|
||||
phantomas.log('lazyLoadableImages: analyzing which images can be lazy-loaded...');
|
||||
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
phantomas.spyEnabled(false, 'analyzing which images can be lazy-loaded');
|
||||
|
||||
var images = document.body.getElementsByTagName('img'),
|
||||
i,
|
||||
len = images.length,
|
||||
offset,
|
||||
path,
|
||||
processedImages = {},
|
||||
src,
|
||||
viewportHeight = window.innerHeight;
|
||||
|
||||
phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight);
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
|
||||
offset = images[i].getBoundingClientRect().top;
|
||||
src = images[i].src;
|
||||
|
||||
// ignore base64-encoded images
|
||||
if (src === '' || /^data:/.test(src)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
path = phantomas.getDOMPath(images[i]);
|
||||
|
||||
// get the most top position for a given image (deduplicate by src)
|
||||
if (typeof processedImages[src] === 'undefined') {
|
||||
processedImages[src] = {
|
||||
offset: offset,
|
||||
path: path
|
||||
};
|
||||
}
|
||||
|
||||
// maybe there's the same image loaded above the fold?
|
||||
if (offset < processedImages[src].offset) {
|
||||
processedImages[src] = {
|
||||
offset: offset,
|
||||
path: path
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length);
|
||||
|
||||
Object.keys(processedImages).forEach(function(src) {
|
||||
var img = processedImages[src];
|
||||
|
||||
if (img.offset > viewportHeight) {
|
||||
phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset);
|
||||
|
||||
phantomas.incrMetric('lazyLoadableImagesBelowTheFold');
|
||||
phantomas.addOffender('lazyLoadableImagesBelowTheFold', src);
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.spyEnabled(true);
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* 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));
|
||||
});
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
exports.version = '0.1';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('scrollExecutionTree');
|
||||
|
||||
phantomas.on('report', function() {
|
||||
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
|
||||
var evt = document.createEvent('CustomEvent');
|
||||
evt.initCustomEvent('scroll', false, false, null);
|
||||
|
||||
function triggerScrollEvent() {
|
||||
phantomas.resetTree();
|
||||
|
||||
try {
|
||||
|
||||
// Chrome triggers them in this order:
|
||||
|
||||
// 1. document
|
||||
phantomas.pushContext({
|
||||
type: 'documentScroll'
|
||||
});
|
||||
document.dispatchEvent(evt);
|
||||
|
||||
// 2. window
|
||||
phantomas.pushContext({
|
||||
type: 'windowScroll'
|
||||
});
|
||||
window.dispatchEvent(evt);
|
||||
|
||||
// No need to call window.onscroll(), it's called by the scroll event on window
|
||||
|
||||
} catch(e) {
|
||||
phantomas.log('ScrollListener error: %s', e);
|
||||
}
|
||||
}
|
||||
|
||||
var firstScrollTime = Date.now();
|
||||
phantomas.log('ScrollListener: triggering a first scroll event...');
|
||||
triggerScrollEvent();
|
||||
|
||||
|
||||
// Ignore the first scroll event and only save the second one,
|
||||
// because we want to detect un-throttled things, throttled ones are ok.
|
||||
var secondScrollTime = Date.now();
|
||||
phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
|
||||
triggerScrollEvent();
|
||||
|
||||
|
||||
var fullTree = phantomas.readFullTree();
|
||||
if (fullTree !== null) {
|
||||
phantomas.setMetric('scrollExecutionTree', true, true);
|
||||
phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
|
||||
phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
|
||||
} else {
|
||||
phantomas.log('Error: scrollExecutionTree could not be extracted');
|
||||
}
|
||||
|
||||
|
||||
phantomas.log('ScrollListener: end of scroll triggering');
|
||||
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
};
|
||||