Compare commits

...
121 Commits
Author SHA1 Message Date
Gaël Métais 484d7ee3b2 Merge pull request #61 from gmetais/develop
V1.3.1
2015-03-18 08:22:41 +01:00
Gaël Métais 7a1bc282e9 v1.3.1 2015-03-18 08:16:48 +01:00
Gaël Métais f45480c0b9 Fix bugs with show warnings and some html escaping 2015-03-18 08:09:51 +01:00
Gaël Métais 23d980910d Increase stack size 2015-03-18 08:09:02 +01:00
Gaël Métais 9b939aa8b4 Remove console.log 2015-03-18 08:06:56 +01:00
Gaël Métais 838a9c3e7d Fix missing bower dependency 2015-03-17 09:44:56 +01:00
Gaël Métais f91de30717 Missing angular dependency 2015-03-17 09:44:11 +01:00
Gaël Métais 0c3299573f Merge pull request #60 from gmetais/develop
Merge V1.3.0
2015-03-17 09:28:26 +01:00
Gaël Métais 13e2d2d597 Fix travis running the tests twice 2015-03-17 08:50:27 +01:00
Gaël Métais a6bbc7119d Update version number 2015-03-17 08:46:31 +01:00
Gaël Métais 20bf58b84f Fix missing space 2015-03-17 08:14:01 +01:00
Gaël Métais fe176c92c0 Update dependencies 2015-03-16 20:00:42 +01:00
Gaël Métais 6009ed53b7 Fix cssParsingError offenders when no line & column are provided 2015-03-16 18:29:42 +01:00
Gaël Métais 79c387ba45 Merge pull request #59 from gmetais/phantomas1.10
Phantomas1.10
2015-03-16 17:43:30 +01:00
Gaël Métais dd767d6664 Fix tests 2015-03-16 17:20:34 +01:00
Gaël Métais 61202f1635 Fix syntax 2015-03-16 17:15:17 +01:00
Gaël Métais 457d6cc54b Add offenders for document.write calls 2015-03-16 16:54:17 +01:00
Gaël Métais bf8123b7c5 Revert eval quickfix 2015-03-16 14:07:54 +01:00
Gaël Métais c98ede9b81 Revert temporary fix for negative cache time 2015-03-16 13:58:42 +01:00
Gaël Métais 34671fca59 Revert fix for jQueryEventTriggers 2015-03-16 13:54:30 +01:00
Gaël Métais 7ad9381aad Revert viewport size 2015-03-16 13:33:23 +01:00
Gaël Métais 7387eefb10 Update phantomas 2015-03-16 13:32:56 +01:00
Gaël Métais ca3403a5b2 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-03-16 13:20:27 +01:00
Gaël Métais 8adc895c8c Fix missing spaces with htmlmin 2015-03-16 13:16:05 +01:00
Gaël Métais 6fe49d06ac Scuse my english 2015-03-16 13:06:56 +01:00
Gaël Métais c47e6d0219 Merge pull request #58 from gmetais/profilerParams
Profiler params
2015-03-16 13:04:23 +01:00
Gaël Métais 99c4b240ec Fix file url in backtrace 2015-03-16 12:54:31 +01:00
Gaël Métais 0947724245 Replace angular templating by homemade for speed purpose 2015-03-16 12:30:05 +01:00
Gaël Métais 15653ef961 Delete jsChildrenDirective not needed anymore 2015-03-16 12:00:56 +01:00
Gaël Métais 893c92266a Fix bug with script filter 2015-03-12 18:07:04 +01:00
Gaël Métais 50571995a4 Speed up timeline page, by removing some angular 2015-03-10 09:34:43 +01:00
Gaël Métais dc2f6e17cd Remove duplicated spying of window.onload 2015-03-10 09:26:07 +01:00
Gaël Métais 9a861b085a Remove touchscreen compatible hover (for perfs) 2015-03-09 12:51:38 +01:00
Gaël Métais 714cb318a9 One way binding 2015-03-09 10:37:00 +01:00
Gaël Métais 2827463b41 Remove unused search field 2015-03-09 07:52:04 +01:00
Gaël Métais 121a8b11b8 Improve arguments reading in timeline 2015-03-08 23:13:09 +01:00
Gaël Métais 3e0e31a5fe New test page 2015-03-08 10:32:57 +01:00
Gaël Métais a7c96f77fe Change the way context is saved in jsExecutionTree 2015-03-02 17:10:21 +01:00
Gaël Métais d3444e9aa0 Add a 'jQuery - find' wrapper on jQuery search by ID 2015-03-02 15:37:11 +01:00
Gaël Métais 71d2e61551 Add some spies on more jQuery functions 2015-03-02 11:23:10 +01:00
Gaël Métais 3756d1185f Better error message 2015-02-27 18:03:43 +01:00
Gaël Métais 5a22eb9542 Fix merge conflict 2015-02-27 17:52:51 +01:00
Gaël Métais cb535f1cee Scroll to the profiler on timeline click 2015-02-27 17:41:54 +01:00
Gaël Métais f304e5efb7 Merge pull request #54 from gmetais/scriptfilter
Introduce a script selector to filter the timeline and the profiler.
2015-02-27 10:04:00 +01:00
Gaël Métais 5732a8ffc8 Spy some more jQuery functions : animations 2015-02-26 19:49:18 +01:00
Gaël Métais 8b7ccb837a Adjust network category score, caching is more important 2015-02-26 19:48:35 +01:00
Gaël Métais f4bb3fc0bc Fix PhantomJS typo in README 2015-02-26 19:46:51 +01:00
Gaël Métais 62602a88bc Remove jQuery from timeline when it's filtered 2015-02-18 23:44:30 +01:00
Gaël Métais f63fa9faf2 Filter timeline by script 2015-02-13 16:32:58 +01:00
Gaël Métais 839bfd4756 Use shorten-url for backtraces 2015-02-13 11:53:10 +01:00
Gaël Métais 9392d21783 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-02-12 18:44:02 +01:00
Gaël Métais ca5594dfbb Forgotten rule cachingNotSpecified 2015-02-12 18:43:40 +01:00
Gaël Métais 1b938417e5 Add doc.createElement and createDocumentFragment to the profiling 2015-02-12 18:43:08 +01:00
Gaël Métais f95e94ac6d Merge pull request #50 from gmetais/subqueries
Show subqueries in timeline
2015-02-12 18:00:12 +01:00
Gaël Métais e9bb72a416 Add sub-queries to the JS profiler table 2015-02-12 17:28:41 +01:00
Gaël Métais 2d003febc1 Front needs the jsTimeline by default 2015-02-09 19:04:47 +01:00
Gaël Métais 42c27af604 No more javascriptExecutionTree bu default (option) 2015-02-09 19:03:04 +01:00
Gaël Métais ee490fc660 Add the --deep-js-analysis option to CLI 2015-02-09 19:01:59 +01:00
Gaël Métais 10500c2fba Fix wrong format 2015-02-09 18:53:32 +01:00
Gaël Métais c4011c0817 Restore deleted screenshot for README 2015-02-06 18:34:33 +01:00
Gaël Métais bc49bebb58 Fix install problems 2015-02-06 18:30:15 +01:00
Gaël Métais dcdd93bcaa Merge pull request #49 from gmetais/dashboard
Merging branch dashboard
2015-02-06 17:11:49 +01:00
Gaël Métais d5fdc3ce63 Fix wording 2015-02-06 16:57:15 +01:00
Gaël Métais c1b5a8625a Missing semicolon 2015-02-06 15:57:13 +01:00
Gaël Métais dd1fd34865 Add screenshot option to CLI 2015-02-06 15:50:15 +01:00
Gaël Métais 9b9e65abd4 Add test to NodeJS module screenshot feature 2015-02-06 15:12:48 +01:00
Gaël Métais e5f917a185 Remove unneeded about page controller 2015-02-06 14:34:45 +01:00
Gaël Métais 368bf4a6d9 Truncate jQuery version, can be long and break display 2015-02-06 14:32:21 +01:00
Gaël Métais a7df7a5436 Forgotten part in cssOldPropertyPrefixes offenders 2015-02-06 14:17:09 +01:00
Gaël Métais d3f5236073 Add an error message when server reboots 2015-02-06 12:32:54 +01:00
Gaël Métais db6fdbc121 Empty URL crashes the server 2015-02-06 12:32:19 +01:00
Gaël Métais dc9aa4d6b4 Fix url not sent 2015-02-06 12:15:20 +01:00
Gaël Métais 5f2553047d Add a warning when max runs per day is reached 2015-02-06 12:03:02 +01:00
Gaël Métais d556f84929 Add a meta description 2015-02-06 11:35:41 +01:00
Gaël Métais 4345ccf356 Fix recommendation 'as many fonts as possible' 2015-02-06 11:19:41 +01:00
Gaël Métais 4032468e10 Fix markdown formatting in npm module description 2015-02-06 11:18:01 +01:00
Gaël Métais 5330f0f8e2 Better links when an error occurs 2015-02-06 11:17:27 +01:00
Gaël Métais 39e6ecc920 Fix url in queue or dashboard pages without http:// 2015-02-06 10:51:27 +01:00
Gaël Métais 928e8f14ea Fix problem when http:// contains upper case 2015-02-06 10:50:36 +01:00
Gaël Métais 0e4ffcee62 Fix error message due to PhantomJS instead of Phantomas 2015-02-06 10:38:32 +01:00
Gaël Métais 955f353a31 Add some keywords on the index page for SEO 2015-02-06 10:19:28 +01:00
Gaël Métais 44df06fac7 v1.2.0 2015-02-06 09:49:39 +01:00
Gaël Métais f099f6f4a9 Change default viewport to 16:9 2015-02-06 09:45:03 +01:00
Gaël Métais e9259356bd Change default viewport to 16:9 2015-02-06 09:44:26 +01:00
Gaël Métais 2264445772 Merge pull request #48 from gmetais/thumbnail
Display a screenshot on dashboard
2015-02-05 23:42:15 +01:00
Gaël Métais 47f0e1f1bf Hover effect on the screenshot on dashboard 2015-02-05 23:17:15 +01:00
Gaël Métais 79d0898fb5 Add a screenshot page 2015-02-05 19:33:58 +01:00
Gaël Métais 85d1119ad6 Add a screenshot page 2015-02-05 19:31:19 +01:00
Gaël Métais 52e73ca7cd Display screenshot on the fashboard 2015-02-05 18:49:59 +01:00
Gaël Métais b5ac42792e Reduce screenshot width 2015-02-05 18:49:42 +01:00
Gaël Métais 6a844e163b Add the screenshot option in front-end 2015-02-05 18:49:05 +01:00
Gaël Métais efa4151850 New API route to retrieve a screenshot 2015-02-05 17:27:21 +01:00
Gaël Métais 55dc387d3c Fix random test fail 2015-02-05 17:19:39 +01:00
Gaël Métais ae47333eef New 'screenshot' option when launching a test 2015-02-05 15:25:17 +01:00
Gaël Métais 5bbe4cde77 Separate steps 2015-02-05 15:23:59 +01:00
Gaël Métais 0ff35d13e8 Fix require 2015-02-05 15:23:05 +01:00
Gaël Métais 3059ab835b Fix some policies 2015-02-03 21:31:04 +01:00
Gaël Métais 798f295912 Remove double jsHint check 2015-02-03 20:23:09 +01:00
Gaël Métais 71d115d161 Merge pull request #47 from softmonkeyjapan/master
Chore : Fix typo on policies
2015-02-03 10:11:31 +01:00
Loic Kartono d5368e5f90 Chore : Fix typo on policies 2015-02-03 10:51:48 +11:00
Gaël Métais d1f0b2e459 Merge pull request #43 from gmetais/phantomas1.9
Phantomas1.9
2015-01-26 20:03:28 +01:00
Gaël Métais ed4ddc0a43 Add CSS colors count and a palette as offender 2015-01-26 19:29:08 +01:00
Gaël Métais 61c09a5b49 Revert custom dev and implement jQueryVersionsLoaded 2015-01-26 15:10:26 +01:00
Gaël Métais a6194f2535 Merge pull request #42 from gmetais/offenders
Fixes #40.
2015-01-26 11:50:51 +01:00
Gaël Métais 901116fde6 Separate offender analyze, parsing in the core, display in the front 2015-01-26 11:34:38 +01:00
Gaël Métais d829525ab8 Update phantomas to v1.9 2015-01-26 11:32:37 +01:00
Gaël Métais ee666a3a6f Temporary fix for negative ttl in cachingTooShort 2015-01-22 15:15:38 +01:00
Gaël Métais 4a8a5829d7 End of the offender transform functions 2015-01-22 14:54:51 +01:00
Gaël Métais fd2271f6d6 Offenders layout for various CSS metrics 2015-01-19 20:31:06 +01:00
Gaël Métais cac36727e8 Handle function names in backtraces 2015-01-19 12:16:56 +01:00
Gaël Métais e6bd0d0006 Strange spaces disapearance in offenders 2015-01-19 12:16:19 +01:00
Gaël Métais 4ef83a844f Fix offendersTransformFn deleted after the first run 2015-01-19 12:15:05 +01:00
Gaël Métais 23dc46d697 Offenders layout for DOMqueriesAvoidable, jsErrors and globalVariables 2015-01-19 11:24:16 +01:00
Gaël Métais 6fc502b0b2 Offenders layout for DOM queries without results 2015-01-17 09:44:50 +01:00
Gaël Métais a4f22a8542 Offenders layout for DOM inserts 2015-01-17 09:21:50 +01:00
Gaël Métais bce7a1ad9a Add the possibility to change offenders' layout 2015-01-16 15:52:56 +01:00
Gaël Métais 49a6e4633a Include angular-sanitize module 2015-01-16 15:51:45 +01:00
Gaël Métais 41c02cb732 Remove coverage from 'grunt test-current-work' command 2015-01-16 15:50:56 +01:00
Gaël Métais 0f00111373 Fix #39 Error 1001 when using the Node module 2015-01-15 12:34:47 +01:00
Gaël Métais 6521d24925 Add quotes to user-agent in debug command line 2015-01-15 12:19:25 +01:00
Gaël Métais 34468198ec Hide ProTip on dashboard if user comes from social sharing 2015-01-12 16:39:54 +01:00
75 changed files with 4860 additions and 641 deletions
+1 -2
View File
@@ -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
View File
@@ -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'
]);
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+12
View File
@@ -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;
+50
View File
@@ -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;
}
-3
View File
@@ -6,6 +6,3 @@
font-size: 0.8em;
margin-bottom: 6em;
}
.queueLink {
color: #FFF;
}
+109
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
+80 -15
View File
@@ -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
View File
@@ -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'
-5
View File
@@ -1,5 +0,0 @@
var aboutCtrl = angular.module('aboutCtrl', []);
aboutCtrl.controller('AboutCtrl', ['$scope', function($scope) {
$scope.about = "this is about YLT";
}]);
+2 -7
View File
@@ -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.
+2 -8
View File
@@ -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);
}
};
}]);
+13 -1
View File
@@ -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;
}
});
}
+2 -8
View File
@@ -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();
}]);
+80 -53
View File
@@ -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 = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
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">&lt;inline CSS&gt;</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
};
});
})();
+27
View File
@@ -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...');
}
});
}
};
}]);
+29 -13
View File
@@ -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 {
+14
View File
@@ -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;
+57
View File
@@ -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;
-4
View File
@@ -6,8 +6,4 @@
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
.queueLink {
color: #FFF;
}
+129
View File
@@ -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;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
+102 -17
View File
@@ -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
View File
@@ -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>
+18 -7
View File
@@ -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">
+19 -1
View File
@@ -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
View File
@@ -1,23 +1,36 @@
<p>Tested url: &nbsp; <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
<p ng-if="url">Tested url: &nbsp; <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
View File
@@ -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>
+14
View File
@@ -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>
+13 -76
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -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
+187
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+135
View File
@@ -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();
+129 -50
View File
@@ -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;
+1 -1
View File
@@ -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');
+40 -9
View File
@@ -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;
+1 -1
View File
@@ -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";
+3
View File
@@ -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
+27
View File
@@ -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);
});
};
@@ -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;
+25 -15
View File
@@ -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
View File
@@ -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"
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 96 KiB

+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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);
}
});
});
});
+47
View File
@@ -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 -1
View File
@@ -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() {
+128
View File
@@ -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);
});
});
});
+403 -9
View File
@@ -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
View File
@@ -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);
});
});
});
+283
View File
@@ -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
});
});
});
});
+8 -6
View File
@@ -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) {
+1 -1
View File
@@ -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);
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+26 -8
View File
@@ -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,
@@ -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;
+193
View File
@@ -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