Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
484d7ee3b2 | ||
|
|
7a1bc282e9 | ||
|
|
f45480c0b9 | ||
|
|
23d980910d | ||
|
|
9b939aa8b4 | ||
|
|
838a9c3e7d | ||
|
|
f91de30717 | ||
|
|
0c3299573f | ||
|
|
13e2d2d597 | ||
|
|
a6bbc7119d | ||
|
|
20bf58b84f | ||
|
|
fe176c92c0 | ||
|
|
6009ed53b7 | ||
|
|
79c387ba45 | ||
|
|
dd767d6664 | ||
|
|
61202f1635 | ||
|
|
457d6cc54b | ||
|
|
bf8123b7c5 | ||
|
|
c98ede9b81 | ||
|
|
34671fca59 | ||
|
|
7ad9381aad | ||
|
|
7387eefb10 | ||
|
|
ca3403a5b2 | ||
|
|
8adc895c8c | ||
|
|
6fe49d06ac | ||
|
|
c47e6d0219 | ||
|
|
99c4b240ec | ||
|
|
0947724245 | ||
|
|
15653ef961 | ||
|
|
893c92266a | ||
|
|
50571995a4 | ||
|
|
dc2f6e17cd | ||
|
|
9a861b085a | ||
|
|
714cb318a9 | ||
|
|
2827463b41 | ||
|
|
121a8b11b8 | ||
|
|
3e0e31a5fe | ||
|
|
a7c96f77fe | ||
|
|
d3444e9aa0 | ||
|
|
71d2e61551 | ||
|
|
3756d1185f | ||
|
|
5a22eb9542 | ||
|
|
cb535f1cee | ||
|
|
f304e5efb7 | ||
|
|
5732a8ffc8 | ||
|
|
8b7ccb837a | ||
|
|
f4bb3fc0bc | ||
|
|
62602a88bc | ||
|
|
f63fa9faf2 | ||
|
|
839bfd4756 | ||
|
|
9392d21783 | ||
|
|
ca5594dfbb | ||
|
|
1b938417e5 | ||
|
|
f95e94ac6d | ||
|
|
e9bb72a416 | ||
|
|
2d003febc1 | ||
|
|
42c27af604 | ||
|
|
ee490fc660 | ||
|
|
10500c2fba | ||
|
|
c4011c0817 | ||
|
|
bc49bebb58 | ||
|
|
dcdd93bcaa | ||
|
|
d5fdc3ce63 | ||
|
|
c1b5a8625a | ||
|
|
dd1fd34865 | ||
|
|
9b9e65abd4 | ||
|
|
e5f917a185 | ||
|
|
368bf4a6d9 | ||
|
|
a7df7a5436 | ||
|
|
d3f5236073 | ||
|
|
db6fdbc121 | ||
|
|
dc9aa4d6b4 | ||
|
|
5f2553047d | ||
|
|
d556f84929 | ||
|
|
4345ccf356 | ||
|
|
4032468e10 | ||
|
|
5330f0f8e2 | ||
|
|
39e6ecc920 | ||
|
|
928e8f14ea | ||
|
|
0e4ffcee62 | ||
|
|
955f353a31 | ||
|
|
44df06fac7 | ||
|
|
f099f6f4a9 | ||
|
|
e9259356bd | ||
|
|
2264445772 | ||
|
|
47f0e1f1bf | ||
|
|
79d0898fb5 | ||
|
|
85d1119ad6 | ||
|
|
52e73ca7cd | ||
|
|
b5ac42792e | ||
|
|
6a844e163b | ||
|
|
efa4151850 | ||
|
|
55dc387d3c | ||
|
|
ae47333eef | ||
|
|
5bbe4cde77 | ||
|
|
0ff35d13e8 | ||
|
|
3059ab835b | ||
|
|
798f295912 | ||
|
|
71d115d161 | ||
|
|
d5368e5f90 | ||
|
|
d1f0b2e459 | ||
|
|
ed4ddc0a43 | ||
|
|
61c09a5b49 | ||
|
|
a6194f2535 | ||
|
|
901116fde6 | ||
|
|
d829525ab8 | ||
|
|
ee666a3a6f | ||
|
|
4a8a5829d7 | ||
|
|
fd2271f6d6 | ||
|
|
cac36727e8 | ||
|
|
e6bd0d0006 | ||
|
|
4ef83a844f | ||
|
|
23dc46d697 | ||
|
|
6fc502b0b2 | ||
|
|
a4f22a8542 | ||
|
|
bce7a1ad9a | ||
|
|
49a6e4633a | ||
|
|
41c02cb732 | ||
|
|
0f00111373 | ||
|
|
6521d24925 | ||
|
|
34468198ec |
+1
-2
@@ -4,5 +4,4 @@ node_js:
|
||||
before_install:
|
||||
- "npm install -g grunt-cli"
|
||||
- "npm install -g phantomjs"
|
||||
install: npm install
|
||||
before_script: grunt test
|
||||
install: npm install
|
||||
+23
-17
@@ -66,7 +66,9 @@ module.exports = function(grunt) {
|
||||
'app/nodeControllers/*.js',
|
||||
'app/public/scripts/*.js',
|
||||
'phantomas_custom/**/*.js',
|
||||
'test/**/*.js',
|
||||
'test/api/*.js',
|
||||
'test/core/*.js',
|
||||
'test/fixtures/*.js',
|
||||
'front/src/js/**/*.js'
|
||||
]
|
||||
},
|
||||
@@ -92,8 +94,10 @@ module.exports = function(grunt) {
|
||||
},
|
||||
coverage: {
|
||||
files: [
|
||||
{src: ['test/**'], dest: 'coverage/'},
|
||||
{src: ['lib/metadata/**'], dest: 'coverage/'}
|
||||
{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: {
|
||||
@@ -139,7 +143,7 @@ module.exports = function(grunt) {
|
||||
options: {
|
||||
reporter: 'spec',
|
||||
},
|
||||
src: ['coverage/test/api/apiTest.js']
|
||||
src: ['test/core/offendersHelpersTest.js']
|
||||
},
|
||||
coverage: {
|
||||
options: {
|
||||
@@ -154,7 +158,7 @@ module.exports = function(grunt) {
|
||||
dev: {
|
||||
NODE_ENV: 'development'
|
||||
},
|
||||
builded: {
|
||||
built: {
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
},
|
||||
@@ -167,7 +171,7 @@ module.exports = function(grunt) {
|
||||
showStack: true
|
||||
}
|
||||
},
|
||||
builded: {
|
||||
built: {
|
||||
options: {
|
||||
port: 8383,
|
||||
server: './bin/server.js',
|
||||
@@ -182,6 +186,13 @@ module.exports = function(grunt) {
|
||||
showStack: true
|
||||
}
|
||||
},
|
||||
'test-current-work': {
|
||||
options: {
|
||||
port: 8387,
|
||||
server: './bin/server.js',
|
||||
showStack: true
|
||||
}
|
||||
},
|
||||
testSuite: {
|
||||
options: {
|
||||
port: 8388,
|
||||
@@ -209,7 +220,8 @@ module.exports = function(grunt) {
|
||||
htmlmin: {
|
||||
options: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: true
|
||||
},
|
||||
main: {
|
||||
files: [{
|
||||
@@ -314,14 +326,13 @@ module.exports = function(grunt) {
|
||||
'express:dev'
|
||||
]);
|
||||
|
||||
grunt.registerTask('builded', [
|
||||
'env:builded',
|
||||
'express:builded'
|
||||
grunt.registerTask('built', [
|
||||
'env:built',
|
||||
'express:built'
|
||||
]);
|
||||
|
||||
grunt.registerTask('test', [
|
||||
'build',
|
||||
'jshint',
|
||||
'express:testSuite',
|
||||
'clean:coverage',
|
||||
'copy-test-server-settings',
|
||||
@@ -336,16 +347,11 @@ module.exports = function(grunt) {
|
||||
]);
|
||||
|
||||
grunt.registerTask('test-current-work', [
|
||||
'build',
|
||||
'jshint',
|
||||
'express:testSuite',
|
||||
'clean:coverage',
|
||||
'copy-test-server-settings',
|
||||
'lineremover:beforeCoverage',
|
||||
'copy:beforeCoverage',
|
||||
'blanket',
|
||||
'copy:coverage',
|
||||
'express:test',
|
||||
'express:test-current-work',
|
||||
'mochaTest:test-current-work',
|
||||
'clean:tmp'
|
||||
]);
|
||||
|
||||
@@ -12,7 +12,7 @@ Online tool that lets you test a webpage and detects **performance** and **front
|
||||
|
||||
## How it works
|
||||
|
||||
The tool loads the given URL via [PhantomasJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.
|
||||
The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct 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!!!
|
||||
|
||||
|
||||
+45
-7
@@ -1,22 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var debug = require('debug')('ylt:cli');
|
||||
var meow = require('meow');
|
||||
var path = require('path');
|
||||
|
||||
var ylt = require('../lib/index');
|
||||
|
||||
var cli = meow({
|
||||
help: [
|
||||
'Usage',
|
||||
' yellowlabtools <url> <options>',
|
||||
'',
|
||||
'Options:',
|
||||
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
|
||||
' --js-deep-analysis When activated, the javascriptExecutionTree will contain sub-requests.',
|
||||
''
|
||||
].join('\n'),
|
||||
pkg: '../package.json'
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Check parameters
|
||||
if (process.argv.length !== 3) {
|
||||
console.error('Incorrect parameters');
|
||||
console.error('\nUsage: ylt <pageUrl>\n');
|
||||
if (cli.input.length < 1) {
|
||||
console.error('Incorrect parameters: url not provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var url = process.argv[2];
|
||||
var url = cli.input[0];
|
||||
|
||||
(function execute(url) {
|
||||
var options = {};
|
||||
|
||||
// Screenshot option
|
||||
var screenshot = cli.flags.screenshot;
|
||||
if (screenshot && (typeof screenshot !== 'string' || screenshot.toLowerCase().indexOf('.png', screenshot.length - 4) === -1)) {
|
||||
console.error('Incorrect parameters: screenshot must be a path that ends with ".png"');
|
||||
process.exit(1);
|
||||
}
|
||||
if (screenshot) {
|
||||
if (path.resolve(screenshot) !== path.normalize(screenshot)) {
|
||||
// It is not an absolute path, so it is relative to the current command-line directory
|
||||
screenshot = path.join(process.cwd(), screenshot);
|
||||
}
|
||||
options.screenshot = cli.flags.screenshot;
|
||||
}
|
||||
|
||||
// Deep JS analysis option
|
||||
if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
|
||||
options.jsDeepAnalysis = true;
|
||||
}
|
||||
|
||||
|
||||
(function execute(url, options) {
|
||||
'use strict';
|
||||
|
||||
ylt(url).
|
||||
ylt(url, options).
|
||||
|
||||
then(function(data) {
|
||||
|
||||
@@ -32,4 +70,4 @@ var url = process.argv[2];
|
||||
|
||||
debug('Test launched...');
|
||||
|
||||
})(url);
|
||||
})(url, options);
|
||||
+8
-3
@@ -1,8 +1,13 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"dependencies": {
|
||||
"angular": "~1.3.8",
|
||||
"angular-route": "~1.3.8",
|
||||
"angular-resource": "~1.3.7"
|
||||
"angular": "~1.3.14",
|
||||
"angular-route": "~1.3.14",
|
||||
"angular-resource": "~1.3.14",
|
||||
"angular-sanitize": "~1.3.14",
|
||||
"angular-animate": "~1.3.14"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "~1.3.8"
|
||||
}
|
||||
}
|
||||
|
||||
+25
-13
@@ -5,7 +5,14 @@
|
||||
text-align: center;
|
||||
}
|
||||
.summary .globalScore {
|
||||
margin-bottom: 3em;
|
||||
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;
|
||||
@@ -22,6 +29,23 @@
|
||||
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 .notations {
|
||||
display: table;
|
||||
width: 80%;
|
||||
@@ -122,18 +146,6 @@
|
||||
.summary .fromShare {
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
.summary .fromShare a {
|
||||
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;
|
||||
}
|
||||
.summary .apiTip {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 4em;
|
||||
|
||||
@@ -13,6 +13,18 @@
|
||||
.launchBtn.disabled {
|
||||
background: #deaca6;
|
||||
}
|
||||
.features {
|
||||
display: table;
|
||||
width: 50%;
|
||||
margin: 8em 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;
|
||||
|
||||
@@ -113,6 +113,56 @@ h1 span {
|
||||
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.desktop {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 0.2em solid #AAA;
|
||||
background: #000;
|
||||
padding: 0.5em;
|
||||
border-top-left-radius: 0.4em;
|
||||
border-top-right-radius: 0.4em;
|
||||
}
|
||||
.screenshotWrapper.desktop:before {
|
||||
position: absolute;
|
||||
width: 15em;
|
||||
height: 0.6em;
|
||||
bottom: -0.75em;
|
||||
left: -1em;
|
||||
background: #CCC;
|
||||
border-bottom-left-radius: 0.2em;
|
||||
border-bottom-right-radius: 0.2em;
|
||||
content: " ";
|
||||
}
|
||||
.screenshotWrapper.desktop:after {
|
||||
position: absolute;
|
||||
width: 0.4em;
|
||||
height: 0.2em;
|
||||
bottom: -0.55em;
|
||||
left: 12.5em;
|
||||
background: lime;
|
||||
content: " ";
|
||||
}
|
||||
.screenshotWrapper.desktop > div {
|
||||
width: 12em;
|
||||
height: 6.75em;
|
||||
overflow: scroll;
|
||||
position: relative;
|
||||
}
|
||||
.screenshotWrapper.desktop .screenshotImage {
|
||||
width: 100%;
|
||||
}
|
||||
.star {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,3 @@
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 6em;
|
||||
}
|
||||
.queueLink {
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
@@ -82,3 +82,112 @@
|
||||
font-size: 3em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.offenders .offenderButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #efe;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
}
|
||||
.offenders .offenderButton.opens {
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
.offenders .offenderButton.opens:after {
|
||||
position: relative;
|
||||
left: 0.5em;
|
||||
content: '\25BC';
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.offenders .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;
|
||||
}
|
||||
.offenders .offenderButton .domTree {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.offenders .offenderButton .domTree > div {
|
||||
margin: 0.5em;
|
||||
}
|
||||
.offenders .offenderButton .domTree > div div {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.offenders .offenderButton .backtrace,
|
||||
.offenders .offenderButton .cssFileAndLine {
|
||||
white-space: nowrap;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.offenders .offenderButton.opens:hover {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: #ffe0cc;
|
||||
z-index: 2;
|
||||
}
|
||||
.offenders .offenderButton.opens:hover > div {
|
||||
display: block;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
/* Checkerboard background */
|
||||
background-color: #ddd;
|
||||
background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa);
|
||||
background-size: 1em 1em;
|
||||
background-position: 0 0, 0.5em 0.5em;
|
||||
}
|
||||
.colorPalette > div {
|
||||
display: inline-block;
|
||||
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;
|
||||
background-color: inherit;
|
||||
left: -0.2em;
|
||||
top: -0.2em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.screenshot.board {
|
||||
text-align: center;
|
||||
}
|
||||
.screenshot .screenshotWrapper {
|
||||
font-size: 2.08333333333333em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
+80
-15
@@ -2,6 +2,19 @@
|
||||
.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;
|
||||
}
|
||||
@@ -39,6 +52,9 @@
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.timeline .interval .color.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.timeline div.interval:hover {
|
||||
background: #9C4274;
|
||||
}
|
||||
@@ -127,20 +143,6 @@
|
||||
border: 1px dotted #aaa;
|
||||
text-align: left;
|
||||
}
|
||||
.slowRequestsLimit {
|
||||
width: 3em;
|
||||
font-size: 1em;
|
||||
text-align: right;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
input.textFilter {
|
||||
box-shadow: none;
|
||||
font-size: 1em;
|
||||
padding: 0 0.2em;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: none;
|
||||
width: 15em;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
@@ -171,12 +173,65 @@ input.textFilter {
|
||||
.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: 1;
|
||||
}
|
||||
.table .showingDetails .child span:hover {
|
||||
background: inherit;
|
||||
}
|
||||
.table .showingDetails .child span:hover .childArgs {
|
||||
display: none;
|
||||
}
|
||||
.table > div > .value {
|
||||
width: 70%;
|
||||
word-break: break-all;
|
||||
@@ -193,6 +248,7 @@ input.textFilter {
|
||||
width: 0.8em;
|
||||
}
|
||||
.detailsOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
top: -3em;
|
||||
@@ -202,13 +258,16 @@ input.textFilter {
|
||||
background: #fff;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.detailsOverlay {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.showDetails .detailsOverlay {
|
||||
display: block;
|
||||
}
|
||||
.detailsOverlay .closeBtn {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
@@ -247,3 +306,9 @@ input.textFilter {
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.warningsFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.warningsFilterOn > div.warning {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
+10
-3
@@ -1,15 +1,19 @@
|
||||
var yltApp = angular.module('YellowLabTools', [
|
||||
'ngRoute',
|
||||
'ngSanitize',
|
||||
'ngAnimate',
|
||||
'indexCtrl',
|
||||
'aboutCtrl',
|
||||
'dashboardCtrl',
|
||||
'queueCtrl',
|
||||
'ruleCtrl',
|
||||
'screenshotCtrl',
|
||||
'timelineCtrl',
|
||||
'runsFactory',
|
||||
'resultsFactory',
|
||||
'apiService',
|
||||
'menuService',
|
||||
'gradeDirective',
|
||||
'offendersDirectives'
|
||||
]);
|
||||
|
||||
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
|
||||
@@ -33,8 +37,7 @@ yltApp.config(['$routeProvider', '$locationProvider',
|
||||
controller: 'QueueCtrl'
|
||||
}).
|
||||
when('/about', {
|
||||
templateUrl: 'views/about.html',
|
||||
controller: 'AboutCtrl'
|
||||
templateUrl: 'views/about.html'
|
||||
}).
|
||||
when('/result/:runId', {
|
||||
templateUrl: 'views/dashboard.html',
|
||||
@@ -44,6 +47,10 @@ yltApp.config(['$routeProvider', '$locationProvider',
|
||||
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'
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
var aboutCtrl = angular.module('aboutCtrl', []);
|
||||
|
||||
aboutCtrl.controller('AboutCtrl', ['$scope', function($scope) {
|
||||
$scope.about = "this is about YLT";
|
||||
}]);
|
||||
@@ -1,6 +1,6 @@
|
||||
var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
|
||||
|
||||
dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'Runs', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, Runs, Menu) {
|
||||
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;
|
||||
@@ -34,12 +34,7 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
Runs.save({
|
||||
url: $scope.result.params.url,
|
||||
waitForResponse: false
|
||||
}, function(data) {
|
||||
$location.path('/queue/' + data.runId);
|
||||
});
|
||||
API.launchTest($scope.result.params.url);
|
||||
};
|
||||
|
||||
/// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
var indexCtrl = angular.module('indexCtrl', []);
|
||||
|
||||
indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'Runs', function($scope, $location, Runs) {
|
||||
indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'API', function($scope, $location, API) {
|
||||
$scope.launchTest = function() {
|
||||
if ($scope.url) {
|
||||
Runs.save({
|
||||
url: $scope.url,
|
||||
waitForResponse: false
|
||||
}, function(data) {
|
||||
$location.path('/queue/' + data.runId);
|
||||
});
|
||||
|
||||
API.launchTest($scope.url);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -1,6 +1,6 @@
|
||||
var queueCtrl = angular.module('queueCtrl', ['runsFactory']);
|
||||
|
||||
queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', function($scope, $routeParams, $location, Runs) {
|
||||
queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', 'API', function($scope, $routeParams, $location, Runs, API) {
|
||||
$scope.runId = $routeParams.runId;
|
||||
|
||||
var numberOfTries = 0;
|
||||
@@ -9,6 +9,8 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs'
|
||||
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 ++;
|
||||
@@ -21,6 +23,16 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs'
|
||||
} 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var ruleCtrl = angular.module('ruleCtrl', []);
|
||||
|
||||
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'Runs', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, Runs) {
|
||||
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);
|
||||
@@ -22,7 +22,6 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
|
||||
|
||||
function init() {
|
||||
$scope.rule = $scope.result.rules[$scope.policyName];
|
||||
$scope.message = $sce.trustAsHtml($scope.rule.policy.message);
|
||||
}
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
@@ -30,12 +29,7 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
Runs.save({
|
||||
url: $scope.result.params.url,
|
||||
waitForResponse: false
|
||||
}, function(data) {
|
||||
$location.path('/queue/' + data.runId);
|
||||
});
|
||||
API.launchTest($scope.result.params.url);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
init();
|
||||
}, function(err) {
|
||||
$scope.error = true;
|
||||
});
|
||||
} else {
|
||||
$scope.result = $rootScope.loadedResult;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
$location.path('/result/' + $scope.runId);
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.launchTest($scope.result.params.url);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
}]);
|
||||
@@ -1,6 +1,6 @@
|
||||
var timelineCtrl = angular.module('timelineCtrl', []);
|
||||
|
||||
timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'Runs', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, Runs) {
|
||||
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);
|
||||
|
||||
@@ -19,19 +19,52 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
||||
}
|
||||
|
||||
function render() {
|
||||
initScriptFiltering();
|
||||
initExecutionTree();
|
||||
initTimeline();
|
||||
$timeout(initProfiler, 100);
|
||||
}
|
||||
|
||||
function initScriptFiltering() {
|
||||
var offenders = $scope.result.rules.jsCount.offendersObj.list;
|
||||
$scope.scripts = [];
|
||||
|
||||
offenders.forEach(function(script) {
|
||||
var filePath = script.file;
|
||||
|
||||
if (filePath.length > 100) {
|
||||
filePath = filePath.substr(0, 98) + '...';
|
||||
}
|
||||
|
||||
var scriptObj = {
|
||||
fullPath: script.file,
|
||||
shortPath: filePath
|
||||
};
|
||||
|
||||
$scope.scripts.push(scriptObj);
|
||||
});
|
||||
}
|
||||
|
||||
function initExecutionTree() {
|
||||
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
|
||||
|
||||
// Detect the last event of all (before filtering) and read time
|
||||
var lastEvent = originalExecutions[originalExecutions.length - 1];
|
||||
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
|
||||
|
||||
// Filter
|
||||
$scope.executionTree = [];
|
||||
|
||||
originalExecutions.forEach(function(node) {
|
||||
|
||||
// Prepare a faster angular search by creating a kind of search index
|
||||
node.searchIndex = (node.data.callDetails) ? [node.data.type].concat(node.data.callDetails.arguments).join('°°') : node.data.type;
|
||||
|
||||
// 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);
|
||||
});
|
||||
@@ -41,8 +74,6 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
||||
|
||||
// Split the timeline into 200 intervals
|
||||
var numberOfIntervals = 199;
|
||||
var lastEvent = $scope.executionTree[$scope.executionTree.length - 1];
|
||||
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
|
||||
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
|
||||
|
||||
// Pre-fill array of as many elements as there are milleseconds
|
||||
@@ -80,66 +111,62 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
||||
$scope.profilerData = $scope.executionTree;
|
||||
}
|
||||
|
||||
|
||||
function parseBacktrace(str) {
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var out = [];
|
||||
var splited = str.split(' / ');
|
||||
splited.forEach(function(trace) {
|
||||
var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
|
||||
if (result && result[2].length > 0) {
|
||||
var filePath = result[2];
|
||||
var chunks = filePath.split('/');
|
||||
var fileName = chunks[chunks.length - 1];
|
||||
|
||||
out.push({
|
||||
fnName: result[1],
|
||||
fileName: fileName,
|
||||
filePath: filePath,
|
||||
line: result[3]
|
||||
});
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
$scope.filter = function(textFilter, scriptName) {
|
||||
|
||||
$scope.changeScript = function() {
|
||||
initExecutionTree();
|
||||
initTimeline();
|
||||
initProfiler();
|
||||
};
|
||||
|
||||
$scope.onNodeDetailsClick = function(node) {
|
||||
var isOpen = node.showDetails;
|
||||
if (!isOpen) {
|
||||
// Close all other nodes
|
||||
$scope.executionTree.forEach(function(currentNode) {
|
||||
currentNode.showDetails = false;
|
||||
});
|
||||
$scope.findLineIndexByTimestamp = function(timestamp) {
|
||||
var lineIndex = 0;
|
||||
|
||||
// Parse the backtrace
|
||||
if (!node.parsedBacktrace) {
|
||||
node.parsedBacktrace = parseBacktrace(node.data.backtrace);
|
||||
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;
|
||||
}
|
||||
}
|
||||
node.showDetails = !isOpen;
|
||||
|
||||
return lineIndex;
|
||||
};
|
||||
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
$location.path('/result/' + $scope.runId);
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
Runs.save({
|
||||
url: $scope.result.params.url,
|
||||
waitForResponse: false
|
||||
}, function(data) {
|
||||
$location.path('/queue/' + data.runId);
|
||||
});
|
||||
API.launchTest($scope.result.params.url);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
|
||||
}]);
|
||||
}]);
|
||||
|
||||
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attributes) {
|
||||
// When the user clicks on the timeline, find the right profiler line and scroll to it
|
||||
element.on('click', function() {
|
||||
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
|
||||
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
|
||||
|
||||
// Animate the background color to "flash" the row
|
||||
lineElement.addClass('highlight');
|
||||
$timeout(function() {
|
||||
$animate.removeClass(lineElement, 'highlight');
|
||||
scope.$digest();
|
||||
}, 50);
|
||||
|
||||
|
||||
window.scrollTo(0, lineElement[0].offsetTop);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -0,0 +1,800 @@
|
||||
(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.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 'error':
|
||||
return args[0];
|
||||
|
||||
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]) {
|
||||
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]) {
|
||||
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 - outerHeight':
|
||||
case 'jQuery - width':
|
||||
case 'jQuery - innerWidth':
|
||||
case 'jQuery - outerWidth':
|
||||
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 - 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 (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 - error':
|
||||
case 'jQuery - focus':
|
||||
case 'jQuery - focusin':
|
||||
case 'jQuery - focusout':
|
||||
case 'jQuery - hover':
|
||||
case 'jQuery - keydown':
|
||||
case 'jQuery - keypress':
|
||||
case 'jQuery - keyup':
|
||||
case 'jQuery - load':
|
||||
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':
|
||||
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;
|
||||
|
||||
case 'jQuery - onDOMReady':
|
||||
return '(function)';
|
||||
|
||||
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) + ':' + 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) {
|
||||
fileAndLine = trace;
|
||||
} else {
|
||||
fnName = withFnResult[1];
|
||||
fileAndLine = withFnResult[2];
|
||||
}
|
||||
|
||||
var fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
|
||||
var filePath = fileAndLineSplit[1];
|
||||
var line = fileAndLineSplit[2];
|
||||
|
||||
out.push({
|
||||
fnName: fnName,
|
||||
filePath: filePath,
|
||||
line: line
|
||||
});
|
||||
});
|
||||
|
||||
} 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.data.type === 'jQuery - bind' && node.data.callDetails.context.length > 5) {
|
||||
html += '<p class="advice">The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).</p>';
|
||||
}
|
||||
|
||||
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 offenders">' + 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 || scope.node.error) {
|
||||
element[0].classList.add('warning');
|
||||
}
|
||||
|
||||
// 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
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,27 @@
|
||||
var apiService = angular.module('apiService', []);
|
||||
|
||||
apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
|
||||
|
||||
return {
|
||||
|
||||
launchTest: function(url) {
|
||||
Runs.save({
|
||||
url: url,
|
||||
waitForResponse: false,
|
||||
screenshot: true,
|
||||
jsTimeline: true
|
||||
}, 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...');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -7,7 +7,16 @@
|
||||
}
|
||||
|
||||
.summary .globalScore {
|
||||
margin-bottom: 3em;
|
||||
display: table;
|
||||
width: 60%;
|
||||
margin: 3em auto;
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
width: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.globalGrade {
|
||||
margin: 0.5 auto;
|
||||
width: 2.5em;
|
||||
@@ -23,6 +32,25 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary .notations {
|
||||
@@ -114,18 +142,6 @@
|
||||
|
||||
.summary .fromShare {
|
||||
margin-bottom: 3em;
|
||||
a {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.summary .apiTip {
|
||||
|
||||
@@ -16,6 +16,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
display: table;
|
||||
width: 50%;
|
||||
margin: 8em 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;
|
||||
|
||||
@@ -112,6 +112,63 @@ h1 span {
|
||||
}
|
||||
}
|
||||
|
||||
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.desktop {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 0.2em solid #AAA;
|
||||
background: #000;
|
||||
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;
|
||||
overflow: scroll;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.screenshotImage {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.star {
|
||||
font-weight: bold;
|
||||
|
||||
@@ -6,8 +6,4 @@
|
||||
.statusSubMessage {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 6em;
|
||||
}
|
||||
|
||||
.queueLink {
|
||||
color: #FFF;
|
||||
}
|
||||
@@ -88,4 +88,133 @@
|
||||
font-size: 3em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.offenders {
|
||||
.offenderButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #efe;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
|
||||
/* 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;
|
||||
|
||||
> 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;
|
||||
background-color: inherit;
|
||||
left: -0.2em;
|
||||
top: -0.2em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.screenshot.board {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screenshot .screenshotWrapper {
|
||||
font-size: 2.08333333333333em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
+102
-17
@@ -14,6 +14,23 @@
|
||||
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;
|
||||
}
|
||||
@@ -49,6 +66,10 @@
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.timeline div.interval:hover {
|
||||
background: #9C4274;
|
||||
@@ -141,22 +162,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.slowRequestsLimit {
|
||||
width: 3em;
|
||||
font-size: 1em;
|
||||
text-align: right;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
|
||||
input.textFilter {
|
||||
box-shadow: none;
|
||||
font-size: 1em;
|
||||
padding: 0 0.2em;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: none;
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
@@ -189,14 +194,82 @@ input.textFilter {
|
||||
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: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table .showingDetails .child span:hover {
|
||||
background: inherit;
|
||||
|
||||
.childArgs {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table > div > .value {
|
||||
width: 70%;
|
||||
word-break: break-all;
|
||||
@@ -215,6 +288,7 @@ input.textFilter {
|
||||
}
|
||||
|
||||
.detailsOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
top: -3em;
|
||||
@@ -224,13 +298,16 @@ input.textFilter {
|
||||
background: #fff;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.detailsOverlay {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.showDetails .detailsOverlay {
|
||||
display: block;
|
||||
}
|
||||
.detailsOverlay .closeBtn {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
@@ -268,4 +345,12 @@ input.textFilter {
|
||||
.execution .icon-warning {
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.warningsFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.warning {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -5,6 +5,7 @@
|
||||
<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">
|
||||
@@ -12,6 +13,7 @@
|
||||
<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">
|
||||
<!-- endbuild -->
|
||||
@@ -20,17 +22,21 @@
|
||||
<script src="/bower_components/angular/angular.min.js"></script>
|
||||
<script src="/bower_components/angular-route/angular-route.min.js"></script>
|
||||
<script src="/bower_components/angular-resource/angular-resource.min.js"></script>
|
||||
<script src="/bower_components/angular-sanitize/angular-sanitize.min.js"></script>
|
||||
<script src="/bower_components/angular-animate/angular-animate.min.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
<script src="/js/controllers/indexCtrl.js"></script>
|
||||
<script src="/js/controllers/aboutCtrl.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/directives/gradeDirective.js"></script>
|
||||
<script src="/js/directives/offendersDirectives.js"></script>
|
||||
<!-- endbuild -->
|
||||
<head>
|
||||
|
||||
|
||||
@@ -2,10 +2,21 @@
|
||||
<div class="summary board">
|
||||
|
||||
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
|
||||
<h2> Global score</h2>
|
||||
<div class="globalScoreDisplay">
|
||||
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
|
||||
<div class="on100">{{globalScore}}/100</div>
|
||||
<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 desktop">
|
||||
<div>
|
||||
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!error" class="apiTip">
|
||||
<div ng-if="!error && !fromSocialShare" class="apiTip">
|
||||
<b>Did you know? Yellow Lab Tools now has an API</b>! <a href="{{getAPIUrl()}}" target="_blank">Here</a> is the JSON output for this run. Checkout <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">the API doc</a>.
|
||||
</div>
|
||||
|
||||
@@ -49,8 +60,8 @@
|
||||
|
||||
<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 href="" ng-click="seeFullReport()">See the full report for this page</a>
|
||||
<a href="/">Test another webpage</a>
|
||||
<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">
|
||||
|
||||
@@ -3,4 +3,22 @@
|
||||
<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}" />
|
||||
</form>
|
||||
</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>
|
||||
+31
-18
@@ -1,23 +1,36 @@
|
||||
<p>Tested url: <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
|
||||
<p ng-if="url">Tested url: <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
|
||||
|
||||
<div ng-if="status.statusCode == 'failed'">
|
||||
<div class="status">Test failed</div>
|
||||
<p class="statusSubMessage">{{status.error}}</p>
|
||||
<p><a class="queueLink" href="https://github.com/gmetais/YellowLabTools/issues" target="_blank">Report a bug on GitHub</a></p>
|
||||
<p><a class="queueLink" href="/">Back to index</a></p>
|
||||
</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 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>
|
||||
<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 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="status.statusCode == 'complete'">
|
||||
<div class="status">Test complete</div>
|
||||
<p class="statusSubMessage">Opening results...</p>
|
||||
<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>
|
||||
+149
-9
@@ -10,23 +10,163 @@
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>Value: {{rule.value}}</h3>
|
||||
<div ng-bind-html="message" class="message"></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">
|
||||
<h3>
|
||||
<ng-pluralize count="rule.offenders.length || 0" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}">
|
||||
</ng-pluralize>
|
||||
</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="offender in rule.offenders track by $index">
|
||||
<div>{{offender}}</div>
|
||||
<div class="offenders" ng-if="rule.policy.hasOffenders">
|
||||
<h3><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 === 'DOMinserts'">
|
||||
<dom-element-button obj="offender.insertedElement"></dom-element-button> appended to <dom-element-button obj="offender.receiverElement"></dom-element-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMqueriesWithoutResults'">
|
||||
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMqueriesAvoidable'">
|
||||
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}: <b>{{offender.count}} queries</b>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'eventsBound'">
|
||||
<b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === '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 === '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 === '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 === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssImports' || 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>
|
||||
<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 === 'cssDuplicatedProperties'">
|
||||
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssEmptyRules'">
|
||||
<b>{{offender.css}} { }</b>
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssExpressions'">
|
||||
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssImportants'">
|
||||
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssOldIEFixes'">
|
||||
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
|
||||
<span ng-bind-html="offender.bolded"></span>
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
|
||||
{{offender.rule}} {<b>{{offender.property}}</b>: {{offender.value + '}' }}
|
||||
<br>
|
||||
<b>{{offender.message}}</b>
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'requests' || policyName === 'htmlCount' || policyName === 'jsCount' || policyName === 'cssCount' || policyName === 'imageCount' || policyName === 'webfontCount' || policyName === 'videoCount' || policyName === 'jsonCount' || policyName === 'otherCount' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
|
||||
<url-link url="offender.file" max-length="100"></url-link>
|
||||
<span ng-if="offender.size || offender.size === 0">({{offender.size}} kB)</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
|
||||
<url-link url="offender" max-length="100"></url-link>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === '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'">
|
||||
{{offender}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
|
||||
|
||||
<div ng-if="policyName === 'DOMelementMaxDepth'">
|
||||
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
|
||||
<p>This is the colors palette, sized by total occurrences:</p>
|
||||
<div class="colorPalette">
|
||||
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div ng-if="!rule && rule !== null" class="notFound">
|
||||
<h2>404</h2>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="screenshot board">
|
||||
<h2>Screenshot</h2>
|
||||
|
||||
<div class="screenshotWrapper desktop">
|
||||
<div>
|
||||
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
</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,5 +1,12 @@
|
||||
<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>
|
||||
|
||||
@@ -18,9 +25,11 @@
|
||||
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
|
||||
domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domComplete
|
||||
}">
|
||||
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
|
||||
<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>
|
||||
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,15 +63,8 @@
|
||||
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
|
||||
<label for="warningsFilterOn">Show warnings only</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" ng-model="textFilterOn" />
|
||||
Filter by
|
||||
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="table">
|
||||
|
||||
<toto data-info="mavariable"></toto>
|
||||
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
@@ -70,74 +72,9 @@
|
||||
<div><!-- details --></div>
|
||||
<div>Timestamp</div>
|
||||
</div>
|
||||
<div ng-if="(!warningsFilterOn || node.warning || node.error) && (!textFilterOn || !textFilter.length || node.searchIndex.indexOf(textFilter) >= 0)"
|
||||
ng-repeat="node in profilerData" ng-class="{
|
||||
showingDetails: node.showDetails,
|
||||
jsError: node.error,
|
||||
windowPerformance: node.windowPerformance
|
||||
}">
|
||||
|
||||
<div class="index">{{$index + 1}}</div>
|
||||
<div class="type">{{node.data.type}}</div>
|
||||
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
|
||||
|
||||
<div class="value">
|
||||
{{node.data.callDetails.arguments[0]}}
|
||||
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
|
||||
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div ng-class="{'icon-question': !node.warning && !node.error, 'icon-warning': node.warning || node.error}"
|
||||
ng-click="onNodeDetailsClick(node)"
|
||||
ng-if="node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance"></div>
|
||||
|
||||
<div class="detailsOverlay" ng-if="node.showDetails">
|
||||
<div class="closeBtn" ng-click="onNodeDetailsClick(node)">✖</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.domElement">
|
||||
<h4>Called on DOM element</h4>
|
||||
<div>{{node.data.callDetails.context.domElement}}</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length === 0">
|
||||
<h4>Called on 0 jQuery element</h4>
|
||||
<p class="advice">Useless function call, as the jQuery object is empty.</p>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
|
||||
<h4>Called on 1 jQuery element</h4>
|
||||
<div>{{node.data.callDetails.context.firstElementPath}}</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="node.data.callDetails.context.length > 1">
|
||||
<h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
|
||||
<p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
|
||||
The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
|
||||
</p>
|
||||
<p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
|
||||
</div>
|
||||
|
||||
<p class="advice" ng-if="node.data.resultsNumber === 0">
|
||||
The query returned 0 results. Could it be unused or dead code?
|
||||
</p>
|
||||
|
||||
<div ng-if="node.parsedBacktrace">
|
||||
<h4>Backtrace</h4>
|
||||
<div class="table">
|
||||
<div ng-repeat="trace in node.parsedBacktrace track by $index">
|
||||
<div>{{trace.fnName || '(anonymous)'}}</div>
|
||||
<div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
|
||||
</div>
|
||||
<div ng-if="node.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
|
||||
<div>can't find any backtrace :/</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ var yellowLabTools = function(url, options) {
|
||||
|
||||
} else {
|
||||
|
||||
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
|
||||
if (url.toLowerCase().indexOf('http://') !== 0 && url.toLowerCase().indexOf('https://') !== 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
|
||||
+799
-66
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@
|
||||
"label": "jQuery version",
|
||||
"policies": {
|
||||
"jQueryVersion": 5,
|
||||
"jQueryDifferentVersions": 0.1
|
||||
"jQueryVersionsLoaded": 0.1
|
||||
}
|
||||
},
|
||||
"cssSyntaxError": {
|
||||
@@ -46,7 +46,8 @@
|
||||
"policies": {
|
||||
"cssRules": 2,
|
||||
"cssComplexSelectors": 2,
|
||||
"cssComplexSelectorsByAttribute": 1.5
|
||||
"cssComplexSelectorsByAttribute": 1.5,
|
||||
"cssColors": 0.5
|
||||
}
|
||||
},
|
||||
"badCSS": {
|
||||
@@ -90,9 +91,10 @@
|
||||
"network": {
|
||||
"label": "Network",
|
||||
"policies": {
|
||||
"notFound": 3,
|
||||
"closedConnections": 3,
|
||||
"multipleRequests": 3,
|
||||
"notFound": 2,
|
||||
"closedConnections": 2,
|
||||
"multipleRequests": 2,
|
||||
"cachingNotSpecified": 1,
|
||||
"cachingDisabled": 1,
|
||||
"cachingTooShort": 1,
|
||||
"domains": 1
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
|
||||
|
||||
var OffendersHelpers = function() {
|
||||
|
||||
this.domPathToArray = function(str) {
|
||||
return str.split(/\s?>\s?/);
|
||||
};
|
||||
|
||||
this.listOfDomArraysToTree = function(listOfDomArrays) {
|
||||
var result = {};
|
||||
|
||||
function recursiveTreeBuilder(tree, domArray) {
|
||||
if (domArray.length > 0) {
|
||||
var currentDomElement = domArray.shift();
|
||||
if (tree === null) {
|
||||
tree = {};
|
||||
}
|
||||
tree[currentDomElement] = recursiveTreeBuilder(tree[currentDomElement] || null, domArray);
|
||||
return tree;
|
||||
} else if (tree === null) {
|
||||
return 1;
|
||||
} else {
|
||||
return tree + 1;
|
||||
}
|
||||
}
|
||||
|
||||
listOfDomArrays.forEach(function(domArray) {
|
||||
result = recursiveTreeBuilder(result, domArray);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
this.domPathToDomElementObj = function(domPath) {
|
||||
|
||||
if (typeof domPath === 'boolean') {
|
||||
return {
|
||||
// Not a normal element path
|
||||
type: 'notAnElement',
|
||||
element: domPath
|
||||
};
|
||||
}
|
||||
|
||||
var domArray = this.domPathToArray(domPath);
|
||||
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
|
||||
|
||||
if (domArray[0] === 'html') {
|
||||
return {
|
||||
type: 'html'
|
||||
};
|
||||
}
|
||||
if (domArray[0] === 'body') {
|
||||
if (domArray.length === 1) {
|
||||
return {
|
||||
type: 'body'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'domElement',
|
||||
element: domArray[domArray.length - 1],
|
||||
tree: domTree
|
||||
};
|
||||
}
|
||||
}
|
||||
if (domArray[0] === 'head') {
|
||||
return {
|
||||
type: 'head'
|
||||
};
|
||||
}
|
||||
if (domArray[0] === '#document') {
|
||||
return {
|
||||
type: 'document'
|
||||
};
|
||||
}
|
||||
if (domArray[0] === 'window') {
|
||||
return {
|
||||
type: 'window'
|
||||
};
|
||||
}
|
||||
if (domArray[0] === 'DocumentFragment') {
|
||||
if (domArray.length === 1) {
|
||||
return {
|
||||
type: 'fragment'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'fragmentElement',
|
||||
element: domArray[domArray.length - 1],
|
||||
tree: domTree
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Not attached element, such as just created with document.createElement()
|
||||
if (domArray.length === 1) {
|
||||
return {
|
||||
type: 'createdElement',
|
||||
element: domPath
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'createdElement',
|
||||
element: domArray[domArray.length - 1],
|
||||
tree: domTree
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.backtraceToArray = function(str) {
|
||||
var traceArray = str.split(/ \/ /);
|
||||
|
||||
if (traceArray) {
|
||||
var results = [];
|
||||
var parts = null;
|
||||
|
||||
for (var i=0 ; i<traceArray.length ; i++) {
|
||||
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
|
||||
|
||||
if (parts) {
|
||||
var obj = {
|
||||
file: parts[3],
|
||||
line: parseInt(parts[4], 10)
|
||||
};
|
||||
|
||||
if (parts[2]) {
|
||||
obj.functionName = parts[2];
|
||||
}
|
||||
|
||||
results.push(obj);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.sortVarsLikeChromeDevTools = function(vars) {
|
||||
return vars.sort(function(a, b) {
|
||||
return (a < b) ? -1 : 1;
|
||||
});
|
||||
};
|
||||
|
||||
this.urlToLink = function(url) {
|
||||
var shortUrl = (url.length > 110) ? url.substr(0, 47) + ' ... ' + url.substr(-48) : url;
|
||||
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortUrl + '</a>';
|
||||
};
|
||||
|
||||
this.cssOffenderPattern = function(offender) {
|
||||
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)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this.fileWithSizePattern = function(fileWithSize) {
|
||||
var parts = /^([^ ]*) \((\d+\.\d{2}) kB\)$/.exec(fileWithSize);
|
||||
|
||||
if (!parts) {
|
||||
return {
|
||||
file: fileWithSize
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
file: parts[1],
|
||||
size: parseFloat(parts[2])
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
module.exports = new OffendersHelpers();
|
||||
+55
-17
@@ -1,3 +1,5 @@
|
||||
var extend = require('util')._extend;
|
||||
|
||||
var debug = require('debug')('ylt:ruleschecker');
|
||||
|
||||
var RulesChecker = function() {
|
||||
@@ -21,31 +23,67 @@ var RulesChecker = function() {
|
||||
|
||||
rule = {
|
||||
value: data.toolsResults[policy.tool].metrics[metricName],
|
||||
policy: policy
|
||||
policy: extend({}, policy) // Clone object policy instead of reference
|
||||
};
|
||||
|
||||
// Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example.
|
||||
if (policy.takeOffendersFrom) {
|
||||
var fromList = policy.takeOffendersFrom;
|
||||
|
||||
// Deal with offenders
|
||||
if (policy.hasOffenders) {
|
||||
|
||||
var offenders = [];
|
||||
|
||||
// takeOffendersFrom option can be a string or an array of strings.
|
||||
if (typeof fromList === 'string') {
|
||||
fromList = [fromList];
|
||||
|
||||
// Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example.
|
||||
if (policy.takeOffendersFrom) {
|
||||
|
||||
var fromList = policy.takeOffendersFrom;
|
||||
|
||||
// takeOffendersFrom option can be a string or an array of strings.
|
||||
if (typeof fromList === 'string') {
|
||||
fromList = [fromList];
|
||||
}
|
||||
|
||||
fromList.forEach(function(from) {
|
||||
if (data.toolsResults[policy.tool] &&
|
||||
data.toolsResults[policy.tool].offenders &&
|
||||
data.toolsResults[policy.tool].offenders[from]) {
|
||||
offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]);
|
||||
}
|
||||
});
|
||||
|
||||
data.toolsResults[policy.tool].offenders[metricName] = offenders;
|
||||
|
||||
} else if (data.toolsResults[policy.tool] &&
|
||||
data.toolsResults[policy.tool].offenders &&
|
||||
data.toolsResults[policy.tool].offenders[metricName]) {
|
||||
offenders = data.toolsResults[policy.tool].offenders[metricName];
|
||||
}
|
||||
|
||||
var offendersObj = {};
|
||||
|
||||
fromList.forEach(function(from) {
|
||||
offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]);
|
||||
});
|
||||
// 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) {
|
||||
|
||||
data.toolsResults[policy.tool].offenders[metricName] = offenders;
|
||||
try {
|
||||
offendersObj = policy.offendersTransformFn(offenders, rule);
|
||||
} catch(err) {
|
||||
debug('Error while transforming offenders for %s', metricName);
|
||||
debug(err);
|
||||
}
|
||||
|
||||
delete rule.policy.offendersTransformFn;
|
||||
|
||||
} else {
|
||||
|
||||
offendersObj = {
|
||||
count: offenders.length,
|
||||
list: offenders
|
||||
};
|
||||
}
|
||||
|
||||
rule.offendersObj = offendersObj;
|
||||
}
|
||||
|
||||
if (data.toolsResults[policy.tool].offenders &&
|
||||
data.toolsResults[policy.tool].offenders[metricName] &&
|
||||
data.toolsResults[policy.tool].offenders[metricName].length > 0) {
|
||||
rule.offenders = data.toolsResults[policy.tool].offenders[metricName];
|
||||
}
|
||||
|
||||
rule.bad = rule.value > policy.isOkThreshold;
|
||||
rule.abnormal = policy.isAbnormalThreshold && rule.value >= policy.isAbnormalThreshold;
|
||||
|
||||
+5
-1
@@ -43,7 +43,11 @@ var Runner = function(params) {
|
||||
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
|
||||
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
|
||||
|
||||
//Finished!
|
||||
return data;
|
||||
|
||||
}).then(function(data) {
|
||||
|
||||
// Finished!
|
||||
deferred.resolve(data);
|
||||
|
||||
}).fail(function(err) {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
var debug = require('debug')('ylt:screenshotHandler');
|
||||
var lwip = require('lwip');
|
||||
var tmp = require('temporary');
|
||||
var Q = require('q');
|
||||
var fs = require('fs');
|
||||
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) {
|
||||
var that = this;
|
||||
|
||||
debug('Starting screenshot transformation');
|
||||
|
||||
return this.openImage(imagePath)
|
||||
|
||||
.then(function(image) {
|
||||
|
||||
return that.resizeImage(image, width);
|
||||
|
||||
})
|
||||
|
||||
.then(this.toBuffer);
|
||||
};
|
||||
|
||||
|
||||
this.openImage = function(imagePath) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
lwip.open(imagePath, function(err, image){
|
||||
if (err) {
|
||||
debug('Could not open imagePath %s', imagePath);
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
debug('Image correctly open');
|
||||
deferred.resolve(image);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
this.resizeImage = function(image, newWidth) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
var currentWidth = image.width();
|
||||
var ratio = newWidth / currentWidth;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
this.toBuffer = function(image) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
image.toBuffer('jpg', {quality: 90}, function(err, buffer){
|
||||
if (err) {
|
||||
debug('Could not save image to buffer');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
debug('Image correctly transformed to buffer');
|
||||
deferred.resolve(buffer);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
this.deleteTmpFileAndFolder = function(tmpFilePath, screenshotTmpFolder) {
|
||||
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.');
|
||||
} else {
|
||||
debug('Screenshot file deleted.');
|
||||
}
|
||||
|
||||
screenshotTmpFolder.rmdir();
|
||||
debug('Screenshot temp folder deleted');
|
||||
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new screenshotHandler();
|
||||
@@ -1,6 +1,8 @@
|
||||
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');
|
||||
@@ -18,16 +20,29 @@ var ApiController = function(app) {
|
||||
// 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
|
||||
partialResult: req.body.partialResult || null,
|
||||
screenshot: req.body.screenshot || false,
|
||||
jsTimeline: req.body.jsTimeline || 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);
|
||||
@@ -49,81 +64,130 @@ var ApiController = function(app) {
|
||||
|
||||
debug('Launching test %s on %s', run.runId, run.params.url);
|
||||
|
||||
ylt(run.params.url)
|
||||
var runOptions = {
|
||||
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
|
||||
jsDeepAnalysis: run.params.jsTimeline
|
||||
};
|
||||
|
||||
.then(function(data) {
|
||||
return ylt(run.params.url, runOptions);
|
||||
|
||||
debug('Success');
|
||||
|
||||
})
|
||||
// Phantomas completed, let's save the screenshot if any
|
||||
.then(function(data) {
|
||||
|
||||
// Save result in datastore
|
||||
data.runId = run.runId;
|
||||
resultsDatastore.saveResult(data)
|
||||
.then(function() {
|
||||
debug('Success');
|
||||
data.runId = run.runId;
|
||||
|
||||
runsDatastore.markAsComplete(run.runId);
|
||||
|
||||
// Send result if the user was waiting
|
||||
if (run.params.waitForResponse) {
|
||||
|
||||
// Some conditional steps are made if there is a screenshot
|
||||
var screenshotPromise = Q.resolve();
|
||||
|
||||
// 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) {
|
||||
debug('Saving results to resultsDatastore failed:');
|
||||
debug(err);
|
||||
if (run.params.screenshot) {
|
||||
|
||||
// Replace the empty promise created earlier with Q.resolve()
|
||||
screenshotPromise = screenshot.toThumbnail(400)
|
||||
|
||||
// Read screenshot
|
||||
.then(function(screenshotBuffer) {
|
||||
|
||||
if (screenshotBuffer) {
|
||||
debug('Image optimized');
|
||||
data.screenshotBuffer = screenshotBuffer;
|
||||
|
||||
res.status(500).send('Saving results failed');
|
||||
});
|
||||
// Official path to get the image
|
||||
data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
|
||||
}
|
||||
|
||||
})
|
||||
// Delete screenshot temporary file
|
||||
.then(screenshot.deleteTmpFile);
|
||||
|
||||
}
|
||||
|
||||
// Let's continue
|
||||
screenshotPromise
|
||||
|
||||
// Save results
|
||||
.then(function() {
|
||||
// Remove uneeded temp screenshot path
|
||||
delete data.params.options.screenshot;
|
||||
|
||||
// Empty javascriptExecutionTree if not needed
|
||||
if (!run.params.jsTimeline) {
|
||||
data.javascriptExecutionTree = {};
|
||||
}
|
||||
|
||||
// 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(400).send('Bad request');
|
||||
|
||||
})
|
||||
|
||||
.finally(function() {
|
||||
queue.remove(run.runId);
|
||||
res.status(500).send('An error occured');
|
||||
});
|
||||
|
||||
}).fail(function(err) {
|
||||
console.error('Error or YLT\'s core instanciation');
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
})
|
||||
|
||||
.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 not want to wait for the response, sending the run ID only
|
||||
|
||||
// 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;
|
||||
@@ -221,6 +285,21 @@ var ApiController = function(app) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -7,7 +7,7 @@ var FrontController = function(app) {
|
||||
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/rule/:policy', '/queue/:runId'];
|
||||
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');
|
||||
|
||||
@@ -9,24 +9,28 @@ function ResultsDatastore() {
|
||||
'use strict';
|
||||
|
||||
var resultFileName = 'results.json';
|
||||
var resultScreenshotName = 'screenshot.jpg';
|
||||
var resultsFolderName = 'results';
|
||||
var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName);
|
||||
|
||||
|
||||
this.saveResult = function(testResults) {
|
||||
var promise = createResultFolder(testResults.runId);
|
||||
|
||||
return createResultFolder(testResults.runId)
|
||||
|
||||
debug('Saving results to disk...');
|
||||
.then(function() {
|
||||
return saveScreenshotIfExists(testResults);
|
||||
})
|
||||
|
||||
promise.then(function() {
|
||||
.then(function() {
|
||||
|
||||
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));
|
||||
});
|
||||
debug('Saving results to disk...');
|
||||
|
||||
return promise;
|
||||
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));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -84,6 +88,33 @@ function ResultsDatastore() {
|
||||
|
||||
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;
|
||||
@@ -65,7 +65,7 @@ function RunsDatastore() {
|
||||
errorMessage = "Error 253: Phantomas config error";
|
||||
break;
|
||||
case '254':
|
||||
errorMessage = "Error 254: page loading failed in Phantomas";
|
||||
errorMessage = "Error 254: page loading failed in PhantomJS";
|
||||
break;
|
||||
case '255':
|
||||
errorMessage = "Error 255: Phantomas error";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var Q = require('q');
|
||||
var debug = require('debug')('ylt:runsQueue');
|
||||
|
||||
|
||||
function RunsQueue() {
|
||||
@@ -11,6 +12,8 @@ function RunsQueue() {
|
||||
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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
var debug = require('debug')('ylt:jsExecutionTransformer');
|
||||
|
||||
var offendersHelpers = require('../offendersHelpers');
|
||||
|
||||
var jsExecutionTransformer = function() {
|
||||
|
||||
this.transform = function(data) {
|
||||
@@ -46,6 +48,22 @@ var jsExecutionTransformer = function() {
|
||||
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
|
||||
break;
|
||||
}
|
||||
|
||||
// Change the list of dom paths into a tree
|
||||
treeRecursiveParser(node, function(node) {
|
||||
|
||||
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
|
||||
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
|
||||
}
|
||||
|
||||
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore') {
|
||||
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]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,6 +75,15 @@ var jsExecutionTransformer = function() {
|
||||
|
||||
return javascriptExecutionTree;
|
||||
};
|
||||
|
||||
function treeRecursiveParser(node, fn) {
|
||||
if (node.children) {
|
||||
node.children.forEach(function(child) {
|
||||
treeRecursiveParser(child, fn);
|
||||
});
|
||||
}
|
||||
fn(node);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = new jsExecutionTransformer();
|
||||
@@ -36,7 +36,7 @@ exports.module = function(phantomas) {
|
||||
phantomas.log('Overwritting phantomas spy function');
|
||||
|
||||
function spy(obj, fn, callbackBefore, callbackAfter) {
|
||||
var origFn = obj[fn];
|
||||
var origFn = obj && obj[fn];
|
||||
|
||||
if (typeof origFn !== 'function') {
|
||||
return false;
|
||||
|
||||
@@ -33,9 +33,6 @@ exports.module = function(phantomas) {
|
||||
phantomas.enterContext({
|
||||
type: 'getElementById',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: '#document'
|
||||
},
|
||||
arguments: ['#' + id]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
@@ -65,7 +62,8 @@ exports.module = function(phantomas) {
|
||||
type: 'getElementsByClassName',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: ['.' + className]
|
||||
},
|
||||
@@ -103,7 +101,8 @@ exports.module = function(phantomas) {
|
||||
type: 'getElementsByTagName',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [tagName]
|
||||
},
|
||||
@@ -145,7 +144,8 @@ exports.module = function(phantomas) {
|
||||
type: 'querySelector',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
@@ -177,7 +177,8 @@ exports.module = function(phantomas) {
|
||||
type: 'querySelectorAll',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
@@ -237,7 +238,8 @@ exports.module = function(phantomas) {
|
||||
type: 'appendChild',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: context
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [appended]
|
||||
},
|
||||
@@ -245,20 +247,25 @@ exports.module = function(phantomas) {
|
||||
});
|
||||
}
|
||||
|
||||
function insertBeforeSpyBefore(child) {
|
||||
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: {
|
||||
domElement: context
|
||||
length: 1,
|
||||
elements: [context]
|
||||
},
|
||||
arguments: [appended]
|
||||
arguments: [
|
||||
appended,
|
||||
referent
|
||||
]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
@@ -270,6 +277,52 @@ exports.module = function(phantomas) {
|
||||
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();
|
||||
});
|
||||
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
@@ -289,7 +342,7 @@ exports.module = function(phantomas) {
|
||||
|
||||
// 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('../../../../../../node_modules/phantomas/lib/collection'),
|
||||
var Collection = require('../../util/collection'),
|
||||
DOMqueries = new Collection();
|
||||
|
||||
phantomas.on('domQuery', function(type, query, fnName, context) {
|
||||
|
||||
@@ -27,7 +27,8 @@ exports.module = function(phantomas) {
|
||||
type: 'addEventListener',
|
||||
callDetails: {
|
||||
context: {
|
||||
domElement: path
|
||||
length: 1,
|
||||
elements: [path]
|
||||
},
|
||||
arguments: [eventType]
|
||||
},
|
||||
|
||||
@@ -7,16 +7,17 @@
|
||||
/* global document: true, window: true */
|
||||
/* jshint -W030 */
|
||||
|
||||
exports.version = '0.2.a';
|
||||
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('jQuerySizzleCallsDuplicated'); // @desc number of calls on the same Sizzle request
|
||||
phantomas.setMetric('jQueryDifferentVersions'); //@desc number of different jQuery versions loaded on the page (not counting iframes)
|
||||
phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
|
||||
|
||||
var jQueryFunctions = [
|
||||
// DOM manipulations
|
||||
@@ -54,7 +55,20 @@ exports.module = function(phantomas) {
|
||||
'scrollLeft',
|
||||
'scrollTop',
|
||||
|
||||
// generic events
|
||||
// Animations
|
||||
'hide',
|
||||
'show',
|
||||
'toggle',
|
||||
'animate',
|
||||
'fadeIn',
|
||||
'fadeOut',
|
||||
'fadeTo',
|
||||
'fadeToggle',
|
||||
'slideDown',
|
||||
'slideUp',
|
||||
'slideToggle',
|
||||
|
||||
// Generic events
|
||||
'on',
|
||||
'off',
|
||||
'live',
|
||||
@@ -65,7 +79,7 @@ exports.module = function(phantomas) {
|
||||
'bind',
|
||||
'unbind',
|
||||
|
||||
// more events
|
||||
// More events
|
||||
'blur',
|
||||
'change',
|
||||
'click',
|
||||
@@ -90,10 +104,9 @@ exports.module = function(phantomas) {
|
||||
'scroll',
|
||||
'select',
|
||||
'submit',
|
||||
'toggle',
|
||||
'unload',
|
||||
|
||||
// attributes
|
||||
// Attributes
|
||||
'attr',
|
||||
'prop',
|
||||
'removeAttr',
|
||||
@@ -102,38 +115,55 @@ exports.module = function(phantomas) {
|
||||
'hasClass',
|
||||
'addClass',
|
||||
'removeClass',
|
||||
'toggleClass'
|
||||
'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.once('init', function() {
|
||||
phantomas.evaluate(function(jQueryFunctions) {
|
||||
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
|
||||
(function(phantomas) {
|
||||
var jQuery;
|
||||
var oldJQuery;
|
||||
|
||||
// TODO: create a helper - phantomas.spyGlobalVar() ?
|
||||
window.__defineSetter__('jQuery', function(val) {
|
||||
phantomas.spyGlobalVar('jQuery', function(jQuery) {
|
||||
var version;
|
||||
var jQueryFn;
|
||||
var oldJQuery = jQuery;
|
||||
|
||||
if (!val || !val.fn) {
|
||||
if (!jQuery || !jQuery.fn) {
|
||||
phantomas.log('jQuery: unable to detect version!');
|
||||
return;
|
||||
}
|
||||
|
||||
version = val.fn.jquery;
|
||||
jQuery = val;
|
||||
jQueryFn = val.fn;
|
||||
// Older jQuery (v?.?) compatibility
|
||||
if (!jQueryFn) {
|
||||
jQueryFn = jQuery;
|
||||
// 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;
|
||||
|
||||
phantomas.log('jQuery: loaded v' + version);
|
||||
phantomas.setMetric('jQueryVersion', version);
|
||||
// report the version of jQuery
|
||||
version = jQuery.fn.jquery;
|
||||
phantomas.emit('jQueryLoaded', version);
|
||||
|
||||
|
||||
phantomas.pushContext({
|
||||
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
|
||||
callDetails: {
|
||||
@@ -141,11 +171,13 @@ exports.module = function(phantomas) {
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
oldJQuery = version;
|
||||
|
||||
// jQuery.ready.promise
|
||||
// works for jQuery 1.8.0+ (released Aug 09 2012)
|
||||
phantomas.spy(val.ready, 'promise', function(func) {
|
||||
phantomas.spy(jQuery.ready, 'promise', function(func) {
|
||||
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
|
||||
phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3));
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'jQuery - onDOMReady',
|
||||
@@ -160,16 +192,16 @@ exports.module = function(phantomas) {
|
||||
|
||||
// Sizzle calls - jQuery.find
|
||||
// works for jQuery 1.3+ (released Jan 13 2009)
|
||||
phantomas.spy(val, 'find', function(selector, context) {
|
||||
phantomas.spy(jQuery, 'find', function(selector, context) {
|
||||
phantomas.incrMetric('jQuerySizzleCalls');
|
||||
phantomas.emit('onSizzleCall', selector + ' (context: ' + (phantomas.getDOMPath(context) || 'unknown') + ')');
|
||||
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'jQuery - find',
|
||||
type: 'jQuery - Sizzle call',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: this.length,
|
||||
firstElementPath: phantomas.getDOMPath(context)
|
||||
length: 1,
|
||||
elements: [phantomas.getDOMPath(context)]
|
||||
},
|
||||
arguments: [selector]
|
||||
},
|
||||
@@ -184,16 +216,69 @@ exports.module = function(phantomas) {
|
||||
}) || 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) {
|
||||
var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
|
||||
|
||||
phantomas.spy(jQueryFn, functionName, function(args) {
|
||||
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)) {
|
||||
@@ -246,13 +331,17 @@ exports.module = function(phantomas) {
|
||||
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,
|
||||
firstElementPath: phantomas.getDOMPath(this[0])
|
||||
elements: elements
|
||||
},
|
||||
arguments: args
|
||||
},
|
||||
@@ -260,45 +349,38 @@ exports.module = function(phantomas) {
|
||||
});
|
||||
|
||||
}, function(result) {
|
||||
phantomas.leaveContext();
|
||||
}) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)');
|
||||
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.__defineGetter__('jQuery', function() {
|
||||
return jQuery;
|
||||
});
|
||||
})(window.__phantomas);
|
||||
}, jQueryFunctions);
|
||||
}, jQueryFunctions, jQueryTraversalFunctions);
|
||||
});
|
||||
|
||||
|
||||
// count Sizzle calls to detect duplicated queries
|
||||
var Collection = require('../../../../../../node_modules/phantomas/lib/collection'),
|
||||
sizzleCalls = new Collection(),
|
||||
jQueryLoading = new Collection();
|
||||
|
||||
phantomas.on('onSizzleCall', function(request) {
|
||||
sizzleCalls.push(request);
|
||||
// store the last resource that was received
|
||||
// try to report where given jQuery version was loaded from
|
||||
phantomas.on('recv', function(entry) {
|
||||
if (entry.isJS) {
|
||||
lastUrl = entry.url;
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.on('jQueryLoaded', function(version) {
|
||||
jQueryLoading.push(version);
|
||||
});
|
||||
phantomas.log('jQuery: loaded v' + version);
|
||||
phantomas.setMetric('jQueryVersion', version);
|
||||
|
||||
phantomas.on('report', function() {
|
||||
sizzleCalls.sort().forEach(function(id, cnt) {
|
||||
if (cnt > 1) {
|
||||
phantomas.incrMetric('jQuerySizzleCallsDuplicated');
|
||||
phantomas.addOffender('jQuerySizzleCallsDuplicated', '%s: %d', id, cnt);
|
||||
}
|
||||
});
|
||||
// report multiple jQuery "instances" (issue #435)
|
||||
phantomas.incrMetric('jQueryVersionsLoaded');
|
||||
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
|
||||
|
||||
jQueryLoading.forEach(function(version) {
|
||||
phantomas.incrMetric('jQueryDifferentVersions');
|
||||
phantomas.addOffender('jQueryDifferentVersions', '%s', version);
|
||||
});
|
||||
phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl);
|
||||
});
|
||||
};
|
||||
|
||||
+25
-13
@@ -4,44 +4,56 @@
|
||||
* @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.1.a';
|
||||
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
|
||||
|
||||
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln
|
||||
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval)
|
||||
// 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.once('init', function() {
|
||||
phantomas.evaluate(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()
|
||||
/*phantomas.spy(window, 'eval', function(code) {
|
||||
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
||||
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
|
||||
});*/
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
@@ -52,6 +64,6 @@ exports.module = function(phantomas) {
|
||||
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
|
||||
});
|
||||
})(window.__phantomas);
|
||||
});
|
||||
}, spyEval);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
|
||||
*/
|
||||
|
||||
exports.version = '0.0';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
/*phantomas.on('recv', function(entry, res) {
|
||||
if (!entry.isJS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Yeah, this is weird, i'm sending the information back to the browser...
|
||||
phantomas.evaluate(function(url) {
|
||||
(function(phantomas) {
|
||||
|
||||
phantomas.pushContext({
|
||||
type: 'script loaded',
|
||||
callDetails: {
|
||||
arguments: [url]
|
||||
}
|
||||
});
|
||||
|
||||
})(window.__phantomas);
|
||||
}, entry.url);
|
||||
});*/
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Push items and count them
|
||||
*/
|
||||
|
||||
function collection() {
|
||||
/* jshint validthis: true */
|
||||
this.items = {};
|
||||
}
|
||||
|
||||
collection.prototype = {
|
||||
push: function(item) {
|
||||
if (typeof this.items[item] === 'undefined') {
|
||||
this.items[item] = {
|
||||
cnt: 1
|
||||
};
|
||||
} else {
|
||||
this.items[item].cnt++;
|
||||
}
|
||||
},
|
||||
|
||||
sort: function() {
|
||||
var newItems = {},
|
||||
sortedKeys;
|
||||
|
||||
// sort in descending order (by cnt)
|
||||
sortedKeys = Object.keys(this.items).sort((function(a, b) {
|
||||
return this.items[b].cnt - this.items[a].cnt;
|
||||
}).bind(this));
|
||||
|
||||
// build new items dictionary
|
||||
sortedKeys.forEach(function(key) {
|
||||
newItems[key] = this.items[key];
|
||||
}, this);
|
||||
|
||||
this.items = newItems;
|
||||
return this;
|
||||
},
|
||||
|
||||
forEach: function(callback) {
|
||||
Object.keys(this.items).forEach(function(key) {
|
||||
callback(key, this.items[key].cnt);
|
||||
}, this);
|
||||
},
|
||||
|
||||
has: function(item) {
|
||||
return (typeof this.items[item] !== 'undefined');
|
||||
},
|
||||
|
||||
getLength: function() {
|
||||
return Object.keys(this.items).length;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = collection;
|
||||
@@ -1,8 +1,9 @@
|
||||
var async = require('async');
|
||||
var Q = require('q');
|
||||
var ps = require('ps-node');
|
||||
var debug = require('debug')('ylt:phantomaswrapper');
|
||||
var phantomas = require('phantomas');
|
||||
var async = require('async');
|
||||
var Q = require('q');
|
||||
var ps = require('ps-node');
|
||||
var path = require('path');
|
||||
var debug = require('debug')('ylt:phantomaswrapper');
|
||||
var phantomas = require('phantomas');
|
||||
|
||||
|
||||
var PhantomasWrapper = function() {
|
||||
@@ -19,17 +20,18 @@ var PhantomasWrapper = function() {
|
||||
this.execute = function(data) {
|
||||
|
||||
var deferred = Q.defer();
|
||||
|
||||
var task = data.params;
|
||||
|
||||
|
||||
var options = {
|
||||
// Cusomizable options
|
||||
timeout: task.options.timeout || 60,
|
||||
'timeout': task.options.timeout || 60,
|
||||
'js-deep-analysis': task.options.jsDeepAnalysis || false,
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36',
|
||||
'screenshot': task.options.screenshot || false,
|
||||
|
||||
// Mandatory
|
||||
reporter: 'json:pretty',
|
||||
'reporter': 'json:pretty',
|
||||
'analyze-css': true,
|
||||
'skip-modules': [
|
||||
'blockDomains', // not needed
|
||||
@@ -38,16 +40,16 @@ var PhantomasWrapper = function() {
|
||||
'eventListeners', // overridden
|
||||
'filmStrip', // not needed
|
||||
'har', // not needed for the moment
|
||||
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT,
|
||||
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
|
||||
'jQuery', // overridden
|
||||
'jserrors', // overridden
|
||||
'pageSource', // not needed
|
||||
'screenshot', // not needed for the moment
|
||||
'waitForSelector', // not needed
|
||||
'windowPerformance' // overriden
|
||||
].join(','),
|
||||
'include-dirs': [
|
||||
'lib/tools/phantomas/custom_modules/core',
|
||||
'lib/tools/phantomas/custom_modules/modules'
|
||||
path.join(__dirname, 'custom_modules/core'),
|
||||
path.join(__dirname, 'custom_modules/modules')
|
||||
].join(',')
|
||||
};
|
||||
|
||||
@@ -55,7 +57,14 @@ var PhantomasWrapper = function() {
|
||||
debug('If you want to reproduce the phantomas task only, copy the following command line:');
|
||||
var optionsString = '';
|
||||
for (var opt in options) {
|
||||
optionsString += ' ' + '--' + opt + '=' + options[opt];
|
||||
var value = options[opt];
|
||||
|
||||
if ((typeof value === 'string' || value instanceof String) && value.indexOf(' ') >= 0) {
|
||||
value = '"' + value + '"';
|
||||
}
|
||||
|
||||
optionsString += ' ' + '--' + opt + '=' + value;
|
||||
|
||||
}
|
||||
debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
|
||||
|
||||
@@ -90,7 +99,7 @@ var PhantomasWrapper = function() {
|
||||
debug('Returning from Phantomas');
|
||||
|
||||
// Adding some YellowLabTools errors here
|
||||
if (json && json.metrics && !json.metrics.javascriptExecutionTree) {
|
||||
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
|
||||
err = 1001;
|
||||
}
|
||||
|
||||
@@ -120,10 +129,11 @@ var PhantomasWrapper = function() {
|
||||
if (err) {
|
||||
debug('All ' + triesNumber + ' attemps failed for the test');
|
||||
deferred.reject(err);
|
||||
|
||||
} else {
|
||||
|
||||
// Success!!!
|
||||
deferred.resolve(json);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+26
-23
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.1",
|
||||
"description": "Online tool to audit a webpage for performance and front-end quality issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/gmetais/YellowLabTools.git"
|
||||
@@ -11,43 +12,45 @@
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"async": "~0.9.0",
|
||||
"body-parser": "~1.10.0",
|
||||
"compression": "~1.2.2",
|
||||
"cors": "^2.5.2",
|
||||
"debug": "~2.1.0",
|
||||
"express": "~4.10.6",
|
||||
"phantomas": "1.8.0",
|
||||
"body-parser": "~1.12.1",
|
||||
"compression": "~1.4.3",
|
||||
"cors": "^2.5.3",
|
||||
"debug": "~2.1.3",
|
||||
"express": "~4.12.2",
|
||||
"lwip": "0.0.6",
|
||||
"meow": "^3.0.0",
|
||||
"phantomas": "1.10.0",
|
||||
"ps-node": "0.0.3",
|
||||
"q": "~1.1.2",
|
||||
"rimraf": "~2.2.8"
|
||||
"rimraf": "~2.3.2",
|
||||
"temporary": "0.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^1.10.0",
|
||||
"chai": "^2.1.2",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-blanket": "^0.0.8",
|
||||
"grunt-contrib-clean": "^0.6.0",
|
||||
"grunt-contrib-concat": "^0.5.0",
|
||||
"grunt-contrib-copy": "^0.7.0",
|
||||
"grunt-contrib-cssmin": "^0.11.0",
|
||||
"grunt-contrib-htmlmin": "^0.3.0",
|
||||
"grunt-contrib-jshint": "^0.10.0",
|
||||
"grunt-contrib-less": "^0.12.0",
|
||||
"grunt-contrib-uglify": "^0.7.0",
|
||||
"grunt-env": "^0.4.2",
|
||||
"grunt-contrib-concat": "^0.5.1",
|
||||
"grunt-contrib-copy": "^0.8.0",
|
||||
"grunt-contrib-cssmin": "^0.12.2",
|
||||
"grunt-contrib-htmlmin": "^0.4.0",
|
||||
"grunt-contrib-jshint": "^0.11.0",
|
||||
"grunt-contrib-less": "^1.0.0",
|
||||
"grunt-contrib-uglify": "^0.8.0",
|
||||
"grunt-env": "^0.4.4",
|
||||
"grunt-express": "^1.4.1",
|
||||
"grunt-filerev": "^2.1.2",
|
||||
"grunt-fontsmith": "^0.9.1",
|
||||
"grunt-inline-angular-templates": "^0.1.5",
|
||||
"grunt-line-remover": "^0.0.2",
|
||||
"grunt-mocha-test": "^0.12.4",
|
||||
"grunt-mocha-test": "^0.12.7",
|
||||
"grunt-replace": "^0.8.0",
|
||||
"grunt-usemin": "^3.0.0",
|
||||
"matchdep": "^0.3.0",
|
||||
"mocha": "^2.1.0",
|
||||
"phantomjs": "^1.9.13",
|
||||
"request": "^2.51.0",
|
||||
"sinon": "^1.12.1",
|
||||
"sinon-chai": "^2.6.0"
|
||||
"mocha": "^2.2.1",
|
||||
"request": "^2.53.0",
|
||||
"sinon": "^1.14.0",
|
||||
"sinon-chai": "^2.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 96 KiB |
@@ -3,7 +3,7 @@
|
||||
# APT-GET
|
||||
sudo apt-get update
|
||||
sudo apt-get install lsb-release libfontconfig1 libfreetype6 -y --force-yes
|
||||
sudo apt-get install curl git python-software-properties -y --force-yes
|
||||
sudo apt-get install curl git python-software-properties build-essential make g++ -y --force-yes
|
||||
|
||||
# Installation of NodeJS
|
||||
sudo add-apt-repository -y ppa:chris-lea/node.js
|
||||
@@ -30,4 +30,4 @@ grunt build
|
||||
# Start the server
|
||||
rm server_config/settings.json
|
||||
cp server_config/settings-prod.json server_config/settings.json
|
||||
NODE_ENV=production forever start -c "node --stack-size=65500" bin/server.js
|
||||
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
|
||||
@@ -23,4 +23,4 @@ npm install phantomjs -g
|
||||
grunt build
|
||||
|
||||
# Restart the server
|
||||
NODE_ENV=production forever start -c "node --stack-size=65500" bin/server.js
|
||||
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
|
||||
+51
-3
@@ -16,6 +16,7 @@ describe('api', function() {
|
||||
|
||||
var syncRunResultUrl;
|
||||
var asyncRunId;
|
||||
var screenshotUrl;
|
||||
|
||||
|
||||
it('should refuse a query with an invalid key', function(done) {
|
||||
@@ -94,7 +95,8 @@ describe('api', function() {
|
||||
url: serverUrl + '/api/runs',
|
||||
body: {
|
||||
url: wwwUrl + '/simple-page.html',
|
||||
waitForResponse: true
|
||||
waitForResponse: true,
|
||||
screenshot: true
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
@@ -131,7 +133,6 @@ describe('api', function() {
|
||||
}, function(error, response, body) {
|
||||
if (!error && response.statusCode === 302) {
|
||||
|
||||
console.log(response.headers.location);
|
||||
response.headers.should.have.a.property('location').that.is.a('string');
|
||||
response.headers.location.should.contain('/rules');
|
||||
|
||||
@@ -158,7 +159,20 @@ describe('api', function() {
|
||||
body.should.have.a.property('scoreProfiles').that.is.an('object');
|
||||
body.should.have.a.property('rules').that.is.an('object');
|
||||
body.should.have.a.property('toolsResults').that.is.an('object');
|
||||
|
||||
// javascriptExecutionTree should only be filled if option jsTimeline is true
|
||||
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
|
||||
body.javascriptExecutionTree.should.deep.equal({});
|
||||
|
||||
// Check if the screenshot temporary file was correctly removed
|
||||
body.params.options.should.not.have.a.property('screenshot');
|
||||
// Check if the screenshot buffer was correctly removed
|
||||
body.should.not.have.a.property('screenshotBuffer');
|
||||
// Check if the screenshot url is here
|
||||
body.should.have.a.property('screenshotUrl');
|
||||
body.screenshotUrl.should.equal('/api/results/' + body.runId + '/screenshot.jpg');
|
||||
|
||||
screenshotUrl = body.screenshotUrl;
|
||||
|
||||
done();
|
||||
|
||||
@@ -177,7 +191,8 @@ describe('api', function() {
|
||||
url: serverUrl + '/api/runs',
|
||||
body: {
|
||||
url: wwwUrl + '/simple-page.html',
|
||||
waitForResponse: false
|
||||
waitForResponse: false,
|
||||
jsTimeline: true
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
@@ -516,4 +531,37 @@ describe('api', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should retrieve the screenshot', function(done) {
|
||||
this.timeout(5000);
|
||||
|
||||
request({
|
||||
method: 'GET',
|
||||
url: serverUrl + screenshotUrl
|
||||
}, function(error, response, body) {
|
||||
if (!error && response.statusCode === 200) {
|
||||
response.headers['content-type'].should.equal('image/jpeg');
|
||||
done();
|
||||
} else {
|
||||
done(error || response.statusCode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should fail on a unexistant screenshot', function(done) {
|
||||
this.timeout(5000);
|
||||
|
||||
request({
|
||||
method: 'GET',
|
||||
url: serverUrl + '/api/results/000000/screenshot.jpg'
|
||||
}, function(error, response, body) {
|
||||
if (!error && response.statusCode === 404) {
|
||||
done();
|
||||
} else {
|
||||
done(error || response.statusCode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
var should = require('chai').should();
|
||||
var resultsDatastore = require('../../lib/server/datastores/resultsDatastore');
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('resultsDatastore', function() {
|
||||
|
||||
var datastore = new resultsDatastore();
|
||||
@@ -71,4 +74,48 @@ describe('resultsDatastore', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
var testId3 = '555555';
|
||||
var testData3 = {
|
||||
runId: testId3,
|
||||
other: {
|
||||
foo: 'foo',
|
||||
bar: 2
|
||||
},
|
||||
screenshotBuffer: fs.readFileSync(path.join(__dirname, '../fixtures/logo-large.png'))
|
||||
};
|
||||
|
||||
it('should store a test with a screenshot', function(done) {
|
||||
|
||||
datastore.saveResult(testData3).then(function() {
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a normal result', function(done) {
|
||||
datastore.getResult(testId3)
|
||||
.then(function(results) {
|
||||
|
||||
results.should.not.have.a.property('screenshot');
|
||||
|
||||
done();
|
||||
})
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should retrieve the saved image', function() {
|
||||
datastore.getScreenshot(testId3)
|
||||
.then(function(imageBuffer) {
|
||||
imageBuffer.should.be.an.instanceof(Buffer);
|
||||
done();
|
||||
})
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
var should = require('chai').should();
|
||||
var runsQueue = require('../../lib/server/datastores/runsQueue.js');
|
||||
var runsQueue = require('../../lib/server/datastores/runsQueue');
|
||||
|
||||
describe('runsQueue', function() {
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
var should = require('chai').should();
|
||||
var ScreenshotHandler = require('../../lib/screenshotHandler');
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('screenshotHandler', function() {
|
||||
|
||||
var imagePath = path.join(__dirname, '../fixtures/logo-large.png');
|
||||
var screenshot, lwipImage;
|
||||
|
||||
|
||||
it('should open an image and return an lwip object', function(done) {
|
||||
ScreenshotHandler.openImage(imagePath)
|
||||
.then(function(image) {
|
||||
lwipImage = image;
|
||||
|
||||
lwipImage.should.be.an('object');
|
||||
lwipImage.width().should.equal(620);
|
||||
lwipImage.height().should.equal(104);
|
||||
|
||||
done();
|
||||
})
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should resize an lwip image', function(done) {
|
||||
ScreenshotHandler.resizeImage(lwipImage, 310)
|
||||
.then(function(image) {
|
||||
lwipImage = image;
|
||||
|
||||
lwipImage.width().should.equal(310);
|
||||
lwipImage.height().should.equal(52);
|
||||
|
||||
done();
|
||||
})
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform a lwip image into a buffer', function(done) {
|
||||
ScreenshotHandler.toBuffer(lwipImage)
|
||||
.then(function(buffer) {
|
||||
buffer.should.be.an.instanceof(Buffer);
|
||||
done();
|
||||
})
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should optimize an image and return a buffered version', function(done) {
|
||||
ScreenshotHandler.optimize(imagePath, 200)
|
||||
.then(function(buffer) {
|
||||
buffer.should.be.an.instanceof(Buffer);
|
||||
done();
|
||||
})
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should provide a temporary file object', function() {
|
||||
screenshot = ScreenshotHandler.getScreenshotTempFile();
|
||||
|
||||
screenshot.should.have.a.property('getTmpFolder').that.is.a('function');
|
||||
screenshot.should.have.a.property('getTmpFilePath').that.is.a('function');
|
||||
screenshot.should.have.a.property('toThumbnail').that.is.a('function');
|
||||
screenshot.should.have.a.property('deleteTmpFile').that.is.a('function');
|
||||
});
|
||||
|
||||
|
||||
it('should have created the temporary folder', function() {
|
||||
var folder = screenshot.getTmpFolder();
|
||||
fs.existsSync(folder.path).should.equal(true);
|
||||
});
|
||||
|
||||
|
||||
it('should respond a temporary file', function() {
|
||||
var file = screenshot.getTmpFilePath();
|
||||
file.should.have.string('/screenshot.png');
|
||||
});
|
||||
|
||||
|
||||
it('should delete the temp folder when there is no file', function(done) {
|
||||
var tmpFolderPath = screenshot;
|
||||
|
||||
screenshot.deleteTmpFile()
|
||||
.delay(1000)
|
||||
.then(function() {
|
||||
fs.existsSync(screenshot.getTmpFolder().path).should.equal(false);
|
||||
done();
|
||||
})
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete the temp folder with the screenshot inside', function(done) {
|
||||
screenshot = ScreenshotHandler.getScreenshotTempFile();
|
||||
var tmpFolderPath = screenshot.getTmpFolder().path;
|
||||
var tmpImagePath = path.join(tmpFolderPath, 'screenshot.png');
|
||||
|
||||
// Copy image
|
||||
var testImage = fs.readFileSync(imagePath);
|
||||
fs.writeFileSync(tmpImagePath, testImage);
|
||||
|
||||
fs.existsSync(tmpImagePath).should.equal(true);
|
||||
|
||||
screenshot.deleteTmpFile()
|
||||
.delay(1000)
|
||||
.then(function() {
|
||||
fs.existsSync(tmpImagePath).should.equal(false);
|
||||
fs.existsSync(tmpFolderPath).should.equal(false);
|
||||
done();
|
||||
})
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,362 @@ var rulesChecker = require('../../lib/rulesChecker');
|
||||
describe('rulesChecker', function() {
|
||||
|
||||
var policies = require('../../lib/metadata/policies.js');
|
||||
var results;
|
||||
|
||||
|
||||
it('should transform DOMelementMaxDepth offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMelementMaxDepth": 3
|
||||
},
|
||||
"offenders": {
|
||||
"DOMelementMaxDepth": [
|
||||
"body > div#foo > span.bar"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMelementMaxDepth');
|
||||
results.DOMelementMaxDepth.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 1,
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#foo": {
|
||||
"span.bar": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform DOMidDuplicated offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMidDuplicated": 2
|
||||
},
|
||||
"offenders": {
|
||||
"DOMidDuplicated": [
|
||||
"colorswitch-30883-30865: 4 occurrences",
|
||||
"foo: 1 occurrences"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMidDuplicated');
|
||||
results.DOMidDuplicated.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 2,
|
||||
"list": [
|
||||
{
|
||||
"id": "colorswitch-30883-30865",
|
||||
"occurrences": 4
|
||||
},
|
||||
{
|
||||
"id": "foo",
|
||||
"occurrences": 1
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform DOMinserts offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMinserts": 4
|
||||
},
|
||||
"offenders": {
|
||||
"DOMinserts": [
|
||||
"\"div\" appended to \"html\"",
|
||||
"\"DocumentFragment > link[0]\" appended to \"head\"",
|
||||
"\"div#Netaff-yh1XbS0vK3NaRGu\" appended to \"body > div#Global\"",
|
||||
"\"img\" appended to \"body\""
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMinserts');
|
||||
results.DOMinserts.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 4,
|
||||
"list": [
|
||||
{
|
||||
"insertedElement": {
|
||||
"type": "createdElement",
|
||||
"element": "div"
|
||||
},
|
||||
"receiverElement": {
|
||||
"type": "html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insertedElement": {
|
||||
"type": "fragmentElement",
|
||||
"element": "link[0]",
|
||||
"tree": {
|
||||
"DocumentFragment": {
|
||||
"link[0]": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"receiverElement": {
|
||||
"type": "head"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insertedElement": {
|
||||
"type": "createdElement",
|
||||
"element": "div#Netaff-yh1XbS0vK3NaRGu"
|
||||
},
|
||||
"receiverElement": {
|
||||
"type": "domElement",
|
||||
"element": "div#Global",
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#Global": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"insertedElement": {
|
||||
"type": "createdElement",
|
||||
"element": "img"
|
||||
},
|
||||
"receiverElement": {
|
||||
"type": "body"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform DOMqueriesWithoutResults offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMqueriesWithoutResults": 2
|
||||
},
|
||||
"offenders": {
|
||||
"DOMqueriesWithoutResults": [
|
||||
"#SearchMenu (in #document) using getElementById",
|
||||
".partnership-link (in body > div#Global > div#Header > ul#MainMenu) using getElementsByClassName"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMqueriesWithoutResults');
|
||||
results.DOMqueriesWithoutResults.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 2,
|
||||
"list": [
|
||||
{
|
||||
"context": {
|
||||
"type": "document"
|
||||
},
|
||||
"fn": "getElementById",
|
||||
"query": "#SearchMenu "
|
||||
},
|
||||
{
|
||||
"context": {
|
||||
"element": "ul#MainMenu",
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#Global": {
|
||||
"div#Header": {
|
||||
"ul#MainMenu": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "domElement"
|
||||
},
|
||||
"fn": "getElementsByClassName",
|
||||
"query": ".partnership-link "
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform DOMqueriesAvoidable offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMqueriesAvoidable": 2
|
||||
},
|
||||
"offenders": {
|
||||
"DOMqueriesDuplicated": [
|
||||
"id \"#j2t-top-cart\" with getElementById (in context #document): 4 queries",
|
||||
"class \".listingResult\" with getElementsByClassName (in context body > div#Global > div#Listing): 4 queries"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMqueriesAvoidable');
|
||||
results.DOMqueriesAvoidable.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 2,
|
||||
"list": [
|
||||
{
|
||||
"query": "#j2t-top-cart",
|
||||
"context": {
|
||||
"type": "document"
|
||||
},
|
||||
"fn": "getElementById ",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"query": ".listingResult",
|
||||
"context": {
|
||||
"type": "domElement",
|
||||
"element": "div#Listing",
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#Global": {
|
||||
"div#Listing": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fn": "getElementsByClassName ",
|
||||
"count": 4
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform eventsBound offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"eventsBound": 2
|
||||
},
|
||||
"offenders": {
|
||||
"eventsBound": [
|
||||
"\"DOMContentLoaded\" bound to \"#document\"",
|
||||
"\"unload\" bound to \"window\"",
|
||||
"\"submit\" bound to \"body > div#Global > div#Header > form#search_mini_form\""
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('eventsBound');
|
||||
results.eventsBound.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 3,
|
||||
"list": [
|
||||
{
|
||||
"element": {
|
||||
"type": "document"
|
||||
},
|
||||
"eventName": "DOMContentLoaded"
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"type": "window"
|
||||
},
|
||||
"eventName": "unload"
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"element": "form#search_mini_form",
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#Global": {
|
||||
"div#Header": {
|
||||
"form#search_mini_form": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "domElement"
|
||||
},
|
||||
"eventName": "submit"
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform jsErrors offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"jsErrors": 2
|
||||
},
|
||||
"offenders": {
|
||||
"jsErrors": [
|
||||
"TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)') - http://asset.easydmp.net/js/collect.js:1160 / callCollecte http://asset.easydmp.net/js/collect.js:1203 / callbackUpdateParams http://asset.easydmp.net/js/collect.js:1135 / http://asset.easydmp.net/js/collect.js:1191",
|
||||
"TypeError: 'undefined' is not an object (evaluating 'd.readyState') - http://me.hunkal.com/p/:3"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('jsErrors');
|
||||
results.jsErrors.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 2,
|
||||
"list": [
|
||||
{
|
||||
"error": "TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)')",
|
||||
"backtrace": [
|
||||
{
|
||||
"file": "http://asset.easydmp.net/js/collect.js",
|
||||
"line": 1160
|
||||
},
|
||||
{
|
||||
"file": "http://asset.easydmp.net/js/collect.js",
|
||||
"line": 1203,
|
||||
"functionName": "callCollecte"
|
||||
},
|
||||
{
|
||||
"file": "http://asset.easydmp.net/js/collect.js",
|
||||
"line": 1135,
|
||||
"functionName": "callbackUpdateParams"
|
||||
},
|
||||
{
|
||||
"file": "http://asset.easydmp.net/js/collect.js",
|
||||
"line": 1191
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"error": "TypeError: 'undefined' is not an object (evaluating 'd.readyState')",
|
||||
"backtrace": [
|
||||
{
|
||||
"file": "http://me.hunkal.com/p/",
|
||||
"line": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should grade correctly jQuery versions', function() {
|
||||
|
||||
|
||||
var versions = {
|
||||
'1.2.9': 0,
|
||||
@@ -52,20 +404,20 @@ describe('rulesChecker', function() {
|
||||
results.should.deep.equals({});
|
||||
|
||||
|
||||
// If jQueryDifferentVersions is 0
|
||||
// If jQueryVersionsLoaded is 0
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"jQueryVersion": "1.6.0",
|
||||
"jQueryDifferentVersions": 0
|
||||
"jQueryVersionsLoaded": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
results.should.not.have.a.property('jQueryVersion');
|
||||
results.should.have.a.property('jQueryDifferentVersions');
|
||||
results.jQueryDifferentVersions.should.have.a.property('score').that.equals(100);
|
||||
results.should.have.a.property('jQueryVersionsLoaded');
|
||||
results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(100);
|
||||
|
||||
|
||||
// If there are more than 1 jQuery version
|
||||
@@ -74,14 +426,56 @@ describe('rulesChecker', function() {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"jQueryVersion": "1.6.0",
|
||||
"jQueryDifferentVersions": 2
|
||||
"jQueryVersionsLoaded": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
results.should.not.have.a.property('jQueryVersion');
|
||||
results.should.have.a.property('jQueryDifferentVersions');
|
||||
results.jQueryDifferentVersions.should.have.a.property('score').that.equals(0);
|
||||
results.jQueryDifferentVersions.should.have.a.property('abnormal').that.equals(true);
|
||||
results.should.have.a.property('jQueryVersionsLoaded');
|
||||
results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(0);
|
||||
results.jQueryVersionsLoaded.should.have.a.property('abnormal').that.equals(true);
|
||||
});
|
||||
|
||||
|
||||
it('should transform cssParsingErrors offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"cssParsingErrors": 2
|
||||
},
|
||||
"offenders": {
|
||||
"cssParsingErrors": [
|
||||
"<http://www.sudexpress.com/skin/frontend/sudexpress/default/css/styles.css> (Error: CSS parsing failed: missing '}' @ 4:1)",
|
||||
"<http://www.sudexpress.com/skin/frontend/sudexpress/default/css/reset.css> (Empty CSS was provided)"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('cssParsingErrors');
|
||||
results.cssParsingErrors.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 2,
|
||||
"list": [
|
||||
{
|
||||
"error": "Error: CSS parsing failed: missing '}'",
|
||||
"file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/styles.css",
|
||||
"line": 4,
|
||||
"column": 1
|
||||
},
|
||||
{
|
||||
"error": "Empty CSS was provided",
|
||||
"file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/reset.css",
|
||||
"line": null,
|
||||
"column": null
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Enough for the moment, to be complete...
|
||||
|
||||
});
|
||||
|
||||
+33
-3
@@ -2,6 +2,8 @@ var chai = require('chai');
|
||||
var sinon = require('sinon');
|
||||
var sinonChai = require('sinon-chai');
|
||||
var should = chai.should();
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var ylt = require('../../lib/index');
|
||||
|
||||
chai.use(sinonChai);
|
||||
@@ -52,7 +54,7 @@ describe('index.js', function() {
|
||||
data.toolsResults.phantomas.should.have.a.property('offenders').that.is.an('object');
|
||||
data.toolsResults.phantomas.offenders.should.have.a.property('DOMelementMaxDepth');
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth.should.have.length(1);
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth[0].should.equal('body > h1[1]');
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth[0].should.equal('body > h1[0]');
|
||||
|
||||
// Test rules
|
||||
data.should.have.a.property('rules').that.is.an('object');
|
||||
@@ -65,14 +67,22 @@ describe('index.js', function() {
|
||||
"message": "<p>A deep DOM makes the CSS matching with DOM elements difficult.</p><p>It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.</p>",
|
||||
"isOkThreshold": 10,
|
||||
"isBadThreshold": 20,
|
||||
"isAbnormalThreshold": 28
|
||||
"isAbnormalThreshold": 28,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"value": 1,
|
||||
"bad": false,
|
||||
"abnormal": false,
|
||||
"score": 100,
|
||||
"abnormalityScore": 0,
|
||||
"offenders": ["body > h1[1]"]
|
||||
"offendersObj": {
|
||||
"count": 1,
|
||||
"tree": {
|
||||
"body": {
|
||||
"h1[0]": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test javascriptExecutionTree
|
||||
@@ -91,4 +101,24 @@ describe('index.js', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should take a screenshot', function(done) {
|
||||
this.timeout(15000);
|
||||
|
||||
var url = 'http://localhost:8388/simple-page.html';
|
||||
var screenshotPath = path.join(__dirname, '../../.tmp/indexTestScreenshot.png');
|
||||
|
||||
ylt(url, {screenshot: screenshotPath})
|
||||
.then(function(data) {
|
||||
|
||||
data.params.options.should.have.a.property('screenshot').that.equals(screenshotPath);
|
||||
data.should.not.have.a.property('screenshotUrl');
|
||||
|
||||
fs.existsSync(screenshotPath).should.equal(true);
|
||||
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
var should = require('chai').should();
|
||||
var offendersHelpers = require('../../lib/offendersHelpers');
|
||||
|
||||
describe('offendersHelpers', function() {
|
||||
|
||||
describe('domPathToArray', function() {
|
||||
|
||||
it('should transform a path to an array', function() {
|
||||
var result = offendersHelpers.domPathToArray('body > section#page > div.alternate-color > ul.retroGuide > li[0] > div.retro-chaine.france2');
|
||||
result.should.deep.equal(['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2']);
|
||||
});
|
||||
|
||||
it('should work even if a space is missing', function() {
|
||||
var result = offendersHelpers.domPathToArray('body > section#page> div.alternate-color > ul.retroGuide >li[0] > div.retro-chaine.france2');
|
||||
result.should.deep.equal(['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('listOfDomArraysToTree', function() {
|
||||
|
||||
it('should transform a list of arrays into a tree', function() {
|
||||
var input = [
|
||||
['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2'],
|
||||
['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2'],
|
||||
['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[1]', 'div.retro-chaine.france2']
|
||||
];
|
||||
|
||||
var inputClone = input.slice();
|
||||
|
||||
var result = offendersHelpers.listOfDomArraysToTree(input);
|
||||
result.should.deep.equal({
|
||||
'body': {
|
||||
'section#page': {
|
||||
'div.alternate-color': {
|
||||
'ul.retroGuide': {
|
||||
'li[0]': {
|
||||
'div.retro-chaine.france2': 2
|
||||
},
|
||||
'li[1]': {
|
||||
'div.retro-chaine.france2': 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
input.should.deep.equal(inputClone);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('domPathToDomElementObj', function() {
|
||||
|
||||
it('should transform html', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('html');
|
||||
result.should.deep.equal({
|
||||
type: 'html'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform body', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('body');
|
||||
result.should.deep.equal({
|
||||
type: 'body'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform head', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('head');
|
||||
result.should.deep.equal({
|
||||
type: 'head'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform #document', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('#document');
|
||||
result.should.deep.equal({
|
||||
type: 'document'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform window', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('window');
|
||||
result.should.deep.equal({
|
||||
type: 'window'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform a standard in-body element', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('body > div#colorbox > div#cboxContent');
|
||||
result.should.deep.equal({
|
||||
type: 'domElement',
|
||||
element: 'div#cboxContent',
|
||||
tree: {
|
||||
'body': {
|
||||
'div#colorbox': {
|
||||
'div#cboxContent': 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform a domFragment element', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('DocumentFragment');
|
||||
result.should.deep.equal({
|
||||
type: 'fragment'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform a domFragment element', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('DocumentFragment > div#colorbox > div#cboxContent');
|
||||
result.should.deep.equal({
|
||||
type: 'fragmentElement',
|
||||
element: 'div#cboxContent',
|
||||
tree: {
|
||||
'DocumentFragment': {
|
||||
'div#colorbox': {
|
||||
'div#cboxContent': 1
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform an not-attached element', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('div#sizcache');
|
||||
result.should.deep.equal({
|
||||
type: 'createdElement',
|
||||
element: 'div#sizcache'
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform an not-attached element path', function() {
|
||||
var result = offendersHelpers.domPathToDomElementObj('div > div#sizcache');
|
||||
result.should.deep.equal({
|
||||
type: 'createdElement',
|
||||
element: 'div#sizcache',
|
||||
tree: {
|
||||
'div': {
|
||||
'div#sizcache': 1
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('backtraceToArray', function() {
|
||||
|
||||
it('should transform a backtrace into an array', function() {
|
||||
var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 / callback http://pouet.com/js/main.js:1');
|
||||
|
||||
result.should.deep.equal([
|
||||
{
|
||||
file: 'http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js',
|
||||
line: 1
|
||||
},
|
||||
{
|
||||
functionName: 'callback',
|
||||
file: 'http://pouet.com/js/main.js',
|
||||
line: 1
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return null if it fails', function() {
|
||||
var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1');
|
||||
|
||||
should.equal(result, null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('sortVarsLikeChromeDevTools', function() {
|
||||
|
||||
it('should sort in the same strange order', function() {
|
||||
var result = offendersHelpers.sortVarsLikeChromeDevTools([
|
||||
'a',
|
||||
'aaa',
|
||||
'a2',
|
||||
'b',
|
||||
'A',
|
||||
'AAA',
|
||||
'B',
|
||||
'_a',
|
||||
'_aaa',
|
||||
'__a',
|
||||
'a_a',
|
||||
'aA',
|
||||
'a__',
|
||||
'$',
|
||||
'$a'
|
||||
]);
|
||||
|
||||
result.should.deep.equal([
|
||||
'$',
|
||||
'$a',
|
||||
'A',
|
||||
'AAA',
|
||||
'B',
|
||||
'__a',
|
||||
'_a',
|
||||
'_aaa',
|
||||
'a',
|
||||
'a2',
|
||||
'aA',
|
||||
'a__',
|
||||
'a_a',
|
||||
'aaa',
|
||||
'b'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('urlToLink', function() {
|
||||
|
||||
it('should transform an url into an html link', function() {
|
||||
var result = offendersHelpers.urlToLink('http://www.google.com/js/main.js');
|
||||
|
||||
result.should.equal('<a href="http://www.google.com/js/main.js" target="_blank" title="http://www.google.com/js/main.js">http://www.google.com/js/main.js</a>');
|
||||
});
|
||||
|
||||
it('should ellypsis the url if too long', function() {
|
||||
var result = offendersHelpers.urlToLink('http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js');
|
||||
|
||||
result.should.equal('<a href="http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js" target="_blank" title="http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js">http://www.google.com/js/longlonglonglonglonglo ... longlonglonglonglonglonglonglonglonglong/main.js</a>');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('cssOffenderPattern', function() {
|
||||
|
||||
it('should transform a css offender into an object', function() {
|
||||
var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li <http://www.pouet.com/css/main.css> @ 30:31862');
|
||||
|
||||
result.should.deep.equal({
|
||||
css: '.pagination .plus ul li',
|
||||
file: 'http://www.pouet.com/css/main.css',
|
||||
line: 30,
|
||||
column: 31862
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with an inline css', function() {
|
||||
var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li [inline CSS] @ 1:32');
|
||||
|
||||
result.should.deep.equal({
|
||||
css: '.pagination .plus ul li',
|
||||
file: null,
|
||||
line: 1,
|
||||
column: 32
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle the case where line and char are not here', function() {
|
||||
var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li');
|
||||
|
||||
result.should.deep.equal({
|
||||
offender: '.pagination .plus ul li'
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('fileWithSizePattern', function() {
|
||||
|
||||
it('should return an object', function() {
|
||||
var result = offendersHelpers.fileWithSizePattern('http://img3.pouet.com/2008/portail/js/jq-timer.js (1.72 kB)');
|
||||
|
||||
result.should.deep.equal({
|
||||
file: 'http://img3.pouet.com/2008/portail/js/jq-timer.js',
|
||||
size: 1.72
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -18,14 +18,15 @@ describe('phantomasWrapper', function() {
|
||||
options: {}
|
||||
}
|
||||
}).then(function(data) {
|
||||
/*jshint -W030 */
|
||||
|
||||
data.should.be.an('object');
|
||||
data.should.have.a.property('generator');
|
||||
data.generator.should.contain('phantomas');
|
||||
data.should.have.a.property('url').that.equals(url);
|
||||
data.should.have.a.property('metrics').that.is.an('object').not.empty();
|
||||
data.should.have.a.property('offenders').that.is.an('object').not.empty();
|
||||
data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty();
|
||||
data.should.have.a.property('metrics').that.is.an('object').not.empty;
|
||||
data.should.have.a.property('offenders').that.is.an('object').not.empty;
|
||||
data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
|
||||
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
@@ -68,14 +69,15 @@ describe('phantomasWrapper', function() {
|
||||
}
|
||||
}
|
||||
}).then(function(data) {
|
||||
/*jshint -W030 */
|
||||
|
||||
data.should.be.an('object');
|
||||
data.should.have.a.property('generator');
|
||||
data.generator.should.contain('phantomas');
|
||||
data.should.have.a.property('url').that.equals(url);
|
||||
data.should.have.a.property('metrics').that.is.an('object').not.empty();
|
||||
data.should.have.a.property('offenders').that.is.an('object').not.empty();
|
||||
data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty();
|
||||
data.should.have.a.property('metrics').that.is.an('object').not.empty;
|
||||
data.should.have.a.property('offenders').that.is.an('object').not.empty;
|
||||
data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
|
||||
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('rulesChecker', function() {
|
||||
|
||||
it('should produce a nice rules object', function() {
|
||||
var data = require('../fixtures/rulesCheckerInput.json');
|
||||
var policies = require('../fixtures/rulesCheckerPolicies.json');
|
||||
var policies = require('../fixtures/rulesCheckerPolicies');
|
||||
var expected = require('../fixtures/rulesCheckerOutput.json');
|
||||
|
||||
var results = rulesChecker.check(data, policies);
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
+26
-8
@@ -6,7 +6,8 @@
|
||||
"message": "A great message",
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"value": 1236,
|
||||
"bad": true,
|
||||
@@ -22,10 +23,14 @@
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": true,
|
||||
"takeOffendersFrom": "metric3"
|
||||
},
|
||||
"value": 222,
|
||||
"offenders": ["offender1", "offender2"],
|
||||
"offendersObj": {
|
||||
"count": 2,
|
||||
"str": "offender1 - offender2"
|
||||
},
|
||||
"bad": false,
|
||||
"abnormal": false,
|
||||
"score": 100,
|
||||
@@ -38,10 +43,14 @@
|
||||
"message": "A great message",
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"value": 6666,
|
||||
"offenders": ["offender1", "offender2"],
|
||||
"offendersObj": {
|
||||
"count": 2,
|
||||
"test": "offender1/offender2"
|
||||
},
|
||||
"bad": true,
|
||||
"abnormal": true,
|
||||
"score": 0,
|
||||
@@ -54,10 +63,14 @@
|
||||
"message": "A great message",
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"value": 1000,
|
||||
"offenders": ["offender3"],
|
||||
"offendersObj": {
|
||||
"count": 1,
|
||||
"list": ["offender3"]
|
||||
},
|
||||
"bad": false,
|
||||
"abnormal": false,
|
||||
"score": 100,
|
||||
@@ -71,10 +84,14 @@
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": true,
|
||||
"takeOffendersFrom": ["metric3", "metric4"]
|
||||
},
|
||||
"value": 3000,
|
||||
"offenders": ["offender1", "offender2", "offender3"],
|
||||
"offendersObj": {
|
||||
"count": 3,
|
||||
"list": ["offender1", "offender2", "offender3"]
|
||||
},
|
||||
"bad": true,
|
||||
"abnormal": false,
|
||||
"score": 0,
|
||||
@@ -118,7 +135,8 @@
|
||||
"message": "<p>This is from another tool!</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 3,
|
||||
"isAbnormalThreshold": 11
|
||||
"isAbnormalThreshold": 11,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"value": 22,
|
||||
"bad": true,
|
||||
|
||||
+32
-9
@@ -1,11 +1,13 @@
|
||||
{
|
||||
var policies = {
|
||||
|
||||
"metric1": {
|
||||
"tool": "tool1",
|
||||
"label": "The metric 1",
|
||||
"message": "A great message",
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"metric2": {
|
||||
"tool": "tool1",
|
||||
@@ -14,7 +16,14 @@
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000,
|
||||
"takeOffendersFrom": "metric3"
|
||||
"takeOffendersFrom": "metric3",
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: 2,
|
||||
str: offenders.join(' - ')
|
||||
};
|
||||
}
|
||||
},
|
||||
"metric3": {
|
||||
"tool": "tool1",
|
||||
@@ -22,7 +31,14 @@
|
||||
"message": "A great message",
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: 2,
|
||||
test: offenders.join('/')
|
||||
};
|
||||
}
|
||||
},
|
||||
"metric4": {
|
||||
"tool": "tool1",
|
||||
@@ -30,7 +46,8 @@
|
||||
"message": "A great message",
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": true,
|
||||
},
|
||||
"metric5": {
|
||||
"tool": "tool1",
|
||||
@@ -39,6 +56,7 @@
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": true,
|
||||
"takeOffendersFrom": ["metric3", "metric4"]
|
||||
},
|
||||
"metric6": {
|
||||
@@ -64,7 +82,8 @@
|
||||
"message": "<p>This is from another tool!</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 3,
|
||||
"isAbnormalThreshold": 11
|
||||
"isAbnormalThreshold": 11,
|
||||
"hasOffenders": false,
|
||||
},
|
||||
|
||||
"unexistantMetric": {
|
||||
@@ -73,12 +92,16 @@
|
||||
"message": "",
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"unexistantTool": {
|
||||
"tool": "unexistant",
|
||||
"isOkThreshold": 1000,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 5000
|
||||
"isAbnormalThreshold": 5000,
|
||||
"hasOffenders": false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = policies;
|
||||
@@ -0,0 +1,193 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Simple page</title>
|
||||
<script src="jquery1.8.3.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simple page</h1>
|
||||
|
||||
<ul>
|
||||
<li>li 1</li>
|
||||
<li>li 2</li>
|
||||
<li>li 3</li>
|
||||
</ul>
|
||||
|
||||
<ul id="foo">
|
||||
<li>li4</li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
|
||||
// Let's start the spaghetti code!!!
|
||||
|
||||
$('ul').find('li');
|
||||
$('li', $('#foo'));
|
||||
var foo = document.getElementById('foo');
|
||||
$('li', foo);
|
||||
foo.getElementsByClassName('bar');
|
||||
document.getElementsByClassName('bar');
|
||||
foo.querySelector('ul li');
|
||||
document.querySelector('ul li');
|
||||
foo.querySelectorAll('ul li');
|
||||
document.querySelectorAll('ul li');
|
||||
|
||||
var li = document.createElement('li');
|
||||
foo.insertBefore(li, foo.children[0]);
|
||||
|
||||
li.addEventListener('click', function() {});
|
||||
|
||||
var $li = $(li);
|
||||
$li.html();
|
||||
$li.html('some content');
|
||||
$('li').html();
|
||||
$li.html('');
|
||||
$('select').html();
|
||||
|
||||
$li.append('<div>woot</div>');
|
||||
$li.append(document.createElement('span'));
|
||||
$li.append($('<b>what</b>'));
|
||||
$li.append(['<div>test1</div>', document.createElement('div')]);
|
||||
$li.append('<div>test3</div>', '<div>test4</div>');
|
||||
|
||||
$('<div>test4</div>').appendTo($li);
|
||||
$('<div>test5</div>').appendTo('<div class="bar"></div>');
|
||||
|
||||
$('li').prepend('<span class="prepended">prepended!</span>');
|
||||
$('<div>test0</div>').prependTo($li);
|
||||
|
||||
$('<div>before bar</div>').before('.bar');
|
||||
$('<div>before $li</div>').insertBefore($li);
|
||||
|
||||
$li.after('textnode after');
|
||||
$('<div>after $li</div>').insertAfter($li);
|
||||
|
||||
$('.prepended').remove('.someClass');
|
||||
$('.prepended').remove();
|
||||
$('.prepended').detach();
|
||||
|
||||
$li.find('div').empty();
|
||||
var $ulClones = $('ul').clone();
|
||||
|
||||
$('span').replaceWith('<span class="newspan">New span</span>');
|
||||
$('<span class="newnewspan">New new span</span>').replaceAll('.newspan');
|
||||
|
||||
$('li').text();
|
||||
$('li').eq(0).text('New li 1');
|
||||
|
||||
$('li').eq(0).wrap('<div></div>');
|
||||
$('ul').wrapAll('<div class="ulWrapper"></div>');
|
||||
$('.ulWrapper').wrapInner('<div class="doubleUlWrapper"></div>');
|
||||
$('.doubleUlWrapper').unwrap();
|
||||
|
||||
$li.css('color', '#F00');
|
||||
$li.css('color');
|
||||
$li.css({'font-size': '1.2em', 'background': '#BFF'});
|
||||
$li.offset();
|
||||
$li.offset({left: 12, top: 23});
|
||||
$li.offset(function() {return {left: 55, top: 44};});
|
||||
$li.height();
|
||||
|
||||
$li.hide();
|
||||
$li.hide(120);
|
||||
$li.hide({duration: 133, done: function() {console.log(12);}});
|
||||
$li.hide(222, 'swing', function() {console.log(9);});
|
||||
$li.toggle();
|
||||
$li.toggle(true);
|
||||
$li.toggle(false);
|
||||
$li.toggle(200);
|
||||
|
||||
var handler = function() {};
|
||||
var $tbody = $('#dataTable tbody');
|
||||
var $tbodytr = $('#dataTable tbody tr');
|
||||
$tbodytr.on('click', handler);
|
||||
$tbody.on('click', 'tr', handler);
|
||||
$tbody.on('click', {someData: 'Joe'}, handler);
|
||||
$tbodytr.off('click', handler);
|
||||
$tbodytr.off('click', 'tr');
|
||||
$tbodytr.off('click');
|
||||
$tbodytr.off();
|
||||
$tbody.delegate('tr', 'click', handler);
|
||||
$tbody.undelegate('tr', 'click');
|
||||
$tbody.undelegate('someNamespage');
|
||||
$tbody.undelegate();
|
||||
$tbody.one('zoop', handler);
|
||||
$tbody.bind('zoop', handler);
|
||||
$tbody.unbind('zoop', handler);
|
||||
$tbody.bind();
|
||||
|
||||
$tbody.blur(handler);
|
||||
$tbody.change(handler);
|
||||
$tbody.click(handler);
|
||||
$tbody.dblclick(handler);
|
||||
$tbody.error(handler);
|
||||
$tbody.focus(handler);
|
||||
$tbody.focusin(handler);
|
||||
$tbody.focusout(handler);
|
||||
$tbody.hover(handler);
|
||||
$tbody.keydown(handler);
|
||||
$tbody.keypress(handler);
|
||||
$tbody.keyup(handler);
|
||||
$tbody.load(handler);
|
||||
$tbody.mousedown(handler);
|
||||
$tbody.mouseenter(handler);
|
||||
$tbody.mouseleave(handler);
|
||||
$tbody.mousemove(handler);
|
||||
$tbody.mouseout(handler);
|
||||
$tbody.mouseover(handler);
|
||||
$tbody.mouseup(handler);
|
||||
$tbody.resize(handler);
|
||||
$tbody.scroll(handler);
|
||||
$tbody.select(handler);
|
||||
$tbody.submit(handler);
|
||||
$tbody.unload(handler);
|
||||
|
||||
$tbody.attr({alt: "Beijing Brush Seller", title: "photo by Kelly Clark"});
|
||||
$tbody.attr('alt');
|
||||
$tbody.attr('alt', 'An alternative alt');
|
||||
$tbody.attr('alt', '');
|
||||
$tbody.removeAttr('alt');
|
||||
|
||||
$tbody.prop({checked: true});
|
||||
$tbody.prop('checked');
|
||||
$tbody.prop('checked', true);
|
||||
$tbody.prop('checked', '');
|
||||
$tbody.removeProp('checked');
|
||||
|
||||
var $input = $('<input type="text"></input>');
|
||||
$input.val('some value');
|
||||
$input.val();
|
||||
|
||||
$tbody.addClass(function() {return 'foo';});
|
||||
$tbody.hasClass('foo');
|
||||
$tbody.removeClass('foo');
|
||||
$tbody.toggleClass('foo');
|
||||
$tbody.toggleClass('foo', true);
|
||||
$tbody.toggleClass();
|
||||
|
||||
$li.children();
|
||||
$li.children('span');
|
||||
$li.closest('ul');
|
||||
$li.closest('ul', document.getElementById('foo'));
|
||||
$li.find('span');
|
||||
$li.find(document.getElementById('foo'));
|
||||
$li.find($li);
|
||||
$li.next();
|
||||
$li.next('li');
|
||||
$li.nextAll();
|
||||
$li.nextAll('li');
|
||||
$li.nextUntil('.theEnd', 'li');
|
||||
$li.offsetParent();
|
||||
$li.prevAll();
|
||||
$li.prevAll('li');
|
||||
$li.prevUntil('.theBeginning', 'li');
|
||||
$li.parent();
|
||||
$li.parent('ul');
|
||||
$li.parents();
|
||||
$li.parents('div');
|
||||
$li.parentsUntil('ul');
|
||||
$li.parentsUntil('body', 'div');
|
||||
$li.siblings();
|
||||
$li.siblings('li');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user