Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13413899eb | ||
|
|
98f074e8d5 | ||
|
|
36a60bae4d | ||
|
|
7c5a5daf2f | ||
|
|
7b74df7f04 | ||
|
|
a6813a7ec1 | ||
|
|
484d7ee3b2 | ||
|
|
7a1bc282e9 | ||
|
|
f45480c0b9 | ||
|
|
23d980910d | ||
|
|
9b939aa8b4 | ||
|
|
838a9c3e7d | ||
|
|
f91de30717 | ||
|
|
0c3299573f | ||
|
|
13e2d2d597 | ||
|
|
a6bbc7119d | ||
|
|
20bf58b84f | ||
|
|
fe176c92c0 | ||
|
|
6009ed53b7 | ||
|
|
79c387ba45 | ||
|
|
dd767d6664 | ||
|
|
61202f1635 | ||
|
|
457d6cc54b | ||
|
|
bf8123b7c5 | ||
|
|
c98ede9b81 | ||
|
|
34671fca59 | ||
|
|
7ad9381aad | ||
|
|
7387eefb10 | ||
|
|
ca3403a5b2 | ||
|
|
8adc895c8c | ||
|
|
6fe49d06ac | ||
|
|
c47e6d0219 | ||
|
|
99c4b240ec | ||
|
|
0947724245 | ||
|
|
15653ef961 | ||
|
|
893c92266a | ||
|
|
50571995a4 | ||
|
|
dc2f6e17cd | ||
|
|
9a861b085a | ||
|
|
714cb318a9 | ||
|
|
2827463b41 | ||
|
|
121a8b11b8 | ||
|
|
3e0e31a5fe | ||
|
|
a7c96f77fe | ||
|
|
d3444e9aa0 | ||
|
|
71d2e61551 | ||
|
|
3756d1185f | ||
|
|
5a22eb9542 | ||
|
|
cb535f1cee | ||
|
|
f304e5efb7 | ||
|
|
5732a8ffc8 | ||
|
|
8b7ccb837a | ||
|
|
f4bb3fc0bc | ||
|
|
62602a88bc | ||
|
|
f63fa9faf2 | ||
|
|
839bfd4756 | ||
|
|
9392d21783 | ||
|
|
ca5594dfbb | ||
|
|
1b938417e5 | ||
|
|
f95e94ac6d | ||
|
|
e9bb72a416 | ||
|
|
2d003febc1 | ||
|
|
42c27af604 | ||
|
|
ee490fc660 | ||
|
|
10500c2fba |
+1
-2
@@ -4,5 +4,4 @@ node_js:
|
|||||||
before_install:
|
before_install:
|
||||||
- "npm install -g grunt-cli"
|
- "npm install -g grunt-cli"
|
||||||
- "npm install -g phantomjs"
|
- "npm install -g phantomjs"
|
||||||
install: npm install
|
install: npm install
|
||||||
before_script: grunt test
|
|
||||||
+14
-11
@@ -66,7 +66,9 @@ module.exports = function(grunt) {
|
|||||||
'app/nodeControllers/*.js',
|
'app/nodeControllers/*.js',
|
||||||
'app/public/scripts/*.js',
|
'app/public/scripts/*.js',
|
||||||
'phantomas_custom/**/*.js',
|
'phantomas_custom/**/*.js',
|
||||||
'test/**/*.js',
|
'test/api/*.js',
|
||||||
|
'test/core/*.js',
|
||||||
|
'test/fixtures/*.js',
|
||||||
'front/src/js/**/*.js'
|
'front/src/js/**/*.js'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -92,10 +94,10 @@ module.exports = function(grunt) {
|
|||||||
},
|
},
|
||||||
coverage: {
|
coverage: {
|
||||||
files: [
|
files: [
|
||||||
{src: ['test/**'], dest: 'coverage/'},
|
{cwd: 'test', src: '**/*', dest: 'coverage/test', expand: true},
|
||||||
{src: ['lib/metadata/**'], dest: 'coverage/'},
|
{cwd: 'lib/metadata', src: '**/*', dest: 'coverage/lib/metadata', expand: true},
|
||||||
{src: ['node_modules/phantomas/**'], dest: 'coverage/'},
|
{cwd: 'node_modules/phantomas', src: '**/*', dest: 'coverage/node_modules/phantomas', expand: true},
|
||||||
{src: ['lib/tools/phantomas/custom_modules/**'], dest: 'coverage/'}
|
{cwd: 'lib/tools/phantomas/custom_modules', src: '**/*', dest: 'coverage/lib/tools/phantomas/custom_modules', expand: true}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
@@ -156,7 +158,7 @@ module.exports = function(grunt) {
|
|||||||
dev: {
|
dev: {
|
||||||
NODE_ENV: 'development'
|
NODE_ENV: 'development'
|
||||||
},
|
},
|
||||||
builded: {
|
built: {
|
||||||
NODE_ENV: 'production'
|
NODE_ENV: 'production'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -169,7 +171,7 @@ module.exports = function(grunt) {
|
|||||||
showStack: true
|
showStack: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builded: {
|
built: {
|
||||||
options: {
|
options: {
|
||||||
port: 8383,
|
port: 8383,
|
||||||
server: './bin/server.js',
|
server: './bin/server.js',
|
||||||
@@ -218,7 +220,8 @@ module.exports = function(grunt) {
|
|||||||
htmlmin: {
|
htmlmin: {
|
||||||
options: {
|
options: {
|
||||||
removeComments: true,
|
removeComments: true,
|
||||||
collapseWhitespace: true
|
collapseWhitespace: true,
|
||||||
|
conservativeCollapse: true
|
||||||
},
|
},
|
||||||
main: {
|
main: {
|
||||||
files: [{
|
files: [{
|
||||||
@@ -323,9 +326,9 @@ module.exports = function(grunt) {
|
|||||||
'express:dev'
|
'express:dev'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
grunt.registerTask('builded', [
|
grunt.registerTask('built', [
|
||||||
'env:builded',
|
'env:built',
|
||||||
'express:builded'
|
'express:built'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
grunt.registerTask('test', [
|
grunt.registerTask('test', [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Online tool that lets you test a webpage and detects **performance** and **front
|
|||||||
|
|
||||||
## How it works
|
## 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!!!
|
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!!!
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -12,7 +12,8 @@ var cli = meow({
|
|||||||
' yellowlabtools <url> <options>',
|
' yellowlabtools <url> <options>',
|
||||||
'',
|
'',
|
||||||
'Options:',
|
'Options:',
|
||||||
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
|
' --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'),
|
].join('\n'),
|
||||||
pkg: '../package.json'
|
pkg: '../package.json'
|
||||||
@@ -44,6 +45,10 @@ if (screenshot) {
|
|||||||
options.screenshot = cli.flags.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) {
|
(function execute(url, options) {
|
||||||
|
|||||||
+5
-4
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "yellowlabtools",
|
"name": "yellowlabtools",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"angular": "~1.3.8",
|
"angular": "~1.3.14",
|
||||||
"angular-route": "~1.3.8",
|
"angular-route": "~1.3.14",
|
||||||
"angular-resource": "~1.3.7",
|
"angular-resource": "~1.3.14",
|
||||||
"angular-sanitize": "~1.4.0-beta.0"
|
"angular-sanitize": "~1.3.14",
|
||||||
|
"angular-animate": "~1.3.14"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"angular": "~1.3.8"
|
"angular": "~1.3.8"
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
border-bottom-left-radius: 0.4em;
|
border-bottom-left-radius: 0.4em;
|
||||||
border-bottom-right-radius: 0.4em;
|
border-bottom-right-radius: 0.4em;
|
||||||
border-top: 1px solid #999;
|
border-top: 1px solid #999;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.offenders .offenderButton .domTree {
|
.offenders .offenderButton .domTree {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -125,13 +126,13 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
.offenders .offenderButton.opens.mouseOver {
|
.offenders .offenderButton.opens:hover {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
background: #ffe0cc;
|
background: #ffe0cc;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
.offenders .offenderButton.opens.mouseOver > div {
|
.offenders .offenderButton.opens:hover > div {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.offendersHtml {
|
.offendersHtml {
|
||||||
|
|||||||
+80
-15
@@ -2,6 +2,19 @@
|
|||||||
.execution {
|
.execution {
|
||||||
text-align: center;
|
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 {
|
.timeline {
|
||||||
margin: 2em 0 5em;
|
margin: 2em 0 5em;
|
||||||
}
|
}
|
||||||
@@ -39,6 +52,9 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.timeline .interval .color.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.timeline div.interval:hover {
|
.timeline div.interval:hover {
|
||||||
background: #9C4274;
|
background: #9C4274;
|
||||||
}
|
}
|
||||||
@@ -127,20 +143,6 @@
|
|||||||
border: 1px dotted #aaa;
|
border: 1px dotted #aaa;
|
||||||
text-align: left;
|
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 {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -171,12 +173,65 @@ input.textFilter {
|
|||||||
.table > div.showingDetails > div {
|
.table > div.showingDetails > div {
|
||||||
background: #f1c40f;
|
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 {
|
.table > div > .index {
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
|
word-break: normal;
|
||||||
}
|
}
|
||||||
.table > div > .type {
|
.table > div > .type {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.table .children {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.6em;
|
||||||
|
}
|
||||||
|
.table .child {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
.table .child > .child {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
.table .child:before {
|
||||||
|
content: "↳";
|
||||||
|
}
|
||||||
|
.table .child .childArgs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.table .child span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.table .child span:hover {
|
||||||
|
background: #EBD8E2;
|
||||||
|
}
|
||||||
|
.table .child span:hover div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.table .child span:hover .childArgs {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
padding: 0 1em 0 2em;
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
background: #EBD8E2;
|
||||||
|
line-height: 1.3em;
|
||||||
|
height: 1.3em;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.table .showingDetails .child span:hover {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
.table .showingDetails .child span:hover .childArgs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.table > div > .value {
|
.table > div > .value {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -193,6 +248,7 @@ input.textFilter {
|
|||||||
width: 0.8em;
|
width: 0.8em;
|
||||||
}
|
}
|
||||||
.detailsOverlay {
|
.detailsOverlay {
|
||||||
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 3em;
|
right: 3em;
|
||||||
top: -3em;
|
top: -3em;
|
||||||
@@ -202,13 +258,16 @@ input.textFilter {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border: 2px solid #f1c40f;
|
border: 2px solid #f1c40f;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 1024px) {
|
@media screen and (max-width: 1024px) {
|
||||||
.detailsOverlay {
|
.detailsOverlay {
|
||||||
width: 25em;
|
width: 25em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.showDetails .detailsOverlay {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.detailsOverlay .closeBtn {
|
.detailsOverlay .closeBtn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5em;
|
top: 0.5em;
|
||||||
@@ -247,3 +306,9 @@ input.textFilter {
|
|||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.warningsFilterOn > div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.warningsFilterOn > div.warning {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
var yltApp = angular.module('YellowLabTools', [
|
var yltApp = angular.module('YellowLabTools', [
|
||||||
'ngRoute',
|
'ngRoute',
|
||||||
'ngSanitize',
|
'ngSanitize',
|
||||||
|
'ngAnimate',
|
||||||
'indexCtrl',
|
'indexCtrl',
|
||||||
'dashboardCtrl',
|
'dashboardCtrl',
|
||||||
'queueCtrl',
|
'queueCtrl',
|
||||||
|
|||||||
@@ -19,19 +19,52 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
initScriptFiltering();
|
||||||
initExecutionTree();
|
initExecutionTree();
|
||||||
initTimeline();
|
initTimeline();
|
||||||
$timeout(initProfiler, 100);
|
$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() {
|
function initExecutionTree() {
|
||||||
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
|
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 = [];
|
$scope.executionTree = [];
|
||||||
|
|
||||||
originalExecutions.forEach(function(node) {
|
originalExecutions.forEach(function(node) {
|
||||||
|
|
||||||
// Prepare a faster angular search by creating a kind of search index
|
// Filter by script (if enabled)
|
||||||
node.searchIndex = (node.data.callDetails) ? [node.data.type].concat(node.data.callDetails.arguments).join('°°') : node.data.type;
|
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);
|
$scope.executionTree.push(node);
|
||||||
});
|
});
|
||||||
@@ -41,8 +74,6 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
|||||||
|
|
||||||
// Split the timeline into 200 intervals
|
// Split the timeline into 200 intervals
|
||||||
var numberOfIntervals = 199;
|
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;
|
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
|
||||||
|
|
||||||
// Pre-fill array of as many elements as there are milleseconds
|
// Pre-fill array of as many elements as there are milleseconds
|
||||||
@@ -80,53 +111,31 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
|||||||
$scope.profilerData = $scope.executionTree;
|
$scope.profilerData = $scope.executionTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.changeScript = function() {
|
||||||
function parseBacktrace(str) {
|
initExecutionTree();
|
||||||
if (!str) {
|
initTimeline();
|
||||||
return null;
|
initProfiler();
|
||||||
}
|
|
||||||
|
|
||||||
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.onNodeDetailsClick = function(node) {
|
$scope.findLineIndexByTimestamp = function(timestamp) {
|
||||||
var isOpen = node.showDetails;
|
var lineIndex = 0;
|
||||||
if (!isOpen) {
|
|
||||||
// Close all other nodes
|
|
||||||
$scope.executionTree.forEach(function(currentNode) {
|
|
||||||
currentNode.showDetails = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse the backtrace
|
for (var i = 0; i < $scope.executionTree.length; i ++) {
|
||||||
if (!node.parsedBacktrace) {
|
var delta = $scope.executionTree[i].data.timestamp - timestamp;
|
||||||
node.parsedBacktrace = parseBacktrace(node.data.backtrace);
|
|
||||||
|
if (delta < $scope.timelineIntervalDuration) {
|
||||||
|
lineIndex = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (delta > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
node.showDetails = !isOpen;
|
|
||||||
|
return lineIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
$scope.backToDashboard = function() {
|
$scope.backToDashboard = function() {
|
||||||
$location.path('/result/' + $scope.runId);
|
$location.path('/result/' + $scope.runId);
|
||||||
};
|
};
|
||||||
@@ -137,4 +146,27 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
|||||||
|
|
||||||
loadResults();
|
loadResults();
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
link: function (scope, element, attributes) {
|
||||||
|
// When the user clicks on the timeline, find the right profiler line and scroll to it
|
||||||
|
element.on('click', function() {
|
||||||
|
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
|
||||||
|
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
|
||||||
|
|
||||||
|
// Animate the background color to "flash" the row
|
||||||
|
lineElement.addClass('highlight');
|
||||||
|
$timeout(function() {
|
||||||
|
$animate.removeClass(lineElement, 'highlight');
|
||||||
|
scope.$digest();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
|
||||||
|
window.scrollTo(0, lineElement[0].offsetTop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
||||||
|
|||||||
@@ -1,50 +1,760 @@
|
|||||||
var offendersDirectives = angular.module('offendersDirectives', []);
|
(function() {
|
||||||
|
"use strict";
|
||||||
|
var offendersDirectives = angular.module('offendersDirectives', []);
|
||||||
|
|
||||||
offendersDirectives.directive('domTree', function() {
|
function getdomTreeHTML(tree) {
|
||||||
return {
|
return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
|
||||||
restrict: 'E',
|
}
|
||||||
scope: {
|
|
||||||
tree: '='
|
function getdomTreeInnerHTML(tree) {
|
||||||
},
|
return recursiveHtmlBuilder(tree);
|
||||||
template: '<div class="domTree"></div>',
|
}
|
||||||
replace: true,
|
|
||||||
link: function(scope, element, attrs) {
|
function recursiveHtmlBuilder(tree) {
|
||||||
|
var html = '';
|
||||||
function recursiveHtmlBuilder(tree) {
|
var keys = Object.keys(tree);
|
||||||
var html = '';
|
|
||||||
var keys = Object.keys(tree);
|
keys.forEach(function(key) {
|
||||||
|
if (isNaN(tree[key])) {
|
||||||
keys.forEach(function(key) {
|
html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
|
||||||
if (isNaN(tree[key])) {
|
} else if (tree[key] > 1) {
|
||||||
html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
|
html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
|
||||||
} else if (tree[key] > 1) {
|
} else {
|
||||||
html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
|
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 'getComputedStyle':
|
||||||
|
return getDomElementButtonHTML(args[0], onASingleLine) + (args[1] || '');
|
||||||
|
|
||||||
|
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 - width':
|
||||||
|
case 'jQuery - innerWidth':
|
||||||
|
case 'jQuery - scrollLeft':
|
||||||
|
case 'jQuery - scrollTop':
|
||||||
|
case 'jQuery - position':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
} else {
|
||||||
|
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - outerHeight':
|
||||||
|
case 'jQuery - outerWidth':
|
||||||
|
if (args[0] && args[0] !== 'true') {
|
||||||
|
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
} else if (args[0] === 'true') {
|
||||||
|
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' (with include margins option)';
|
||||||
|
} else {
|
||||||
|
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - toggle':
|
||||||
|
if (args[0] === 'true') {
|
||||||
|
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
|
||||||
|
} else if (args[0] === 'false') {
|
||||||
|
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
|
||||||
|
} else {
|
||||||
|
return getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - on':
|
||||||
|
case 'jQuery - one':
|
||||||
|
if (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 {
|
} else {
|
||||||
html += '<div><span>' + key + '</span></div>';
|
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
return 'unbind all events';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
return html;
|
case 'jQuery - live':
|
||||||
|
case 'jQuery - bind':
|
||||||
|
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
|
||||||
|
case 'jQuery - die':
|
||||||
|
case 'jQuery - unbind':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
} else {
|
||||||
|
return 'unbind all events';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - delegate':
|
||||||
|
return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
|
||||||
|
|
||||||
|
case 'jQuery - undelegate':
|
||||||
|
if (args[0]) {
|
||||||
|
if (args[1]) {
|
||||||
|
return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'unbind namespace <b>' + args[0] + '</b>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'unbind all events';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - blur':
|
||||||
|
case 'jQuery - change':
|
||||||
|
case 'jQuery - click':
|
||||||
|
case 'jQuery - dblclick':
|
||||||
|
case 'jQuery - focus':
|
||||||
|
case 'jQuery - keydown':
|
||||||
|
case 'jQuery - keypress':
|
||||||
|
case 'jQuery - keyup':
|
||||||
|
case 'jQuery - mousedown':
|
||||||
|
case 'jQuery - mouseenter':
|
||||||
|
case 'jQuery - mouseleave':
|
||||||
|
case 'jQuery - mousemove':
|
||||||
|
case 'jQuery - mouseout':
|
||||||
|
case 'jQuery - mouseover':
|
||||||
|
case 'jQuery - mouseup':
|
||||||
|
case 'jQuery - resize':
|
||||||
|
case 'jQuery - scroll':
|
||||||
|
case 'jQuery - select':
|
||||||
|
case 'jQuery - submit':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
} else {
|
||||||
|
return 'triggered on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - error':
|
||||||
|
case 'jQuery - focusin':
|
||||||
|
case 'jQuery - focusout':
|
||||||
|
case 'jQuery - hover':
|
||||||
|
case 'jQuery - load':
|
||||||
|
case 'jQuery - unload':
|
||||||
|
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
|
||||||
|
case 'jQuery - removeAttr':
|
||||||
|
case 'jQuery - removeProp':
|
||||||
|
return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
|
||||||
|
case 'jQuery - val':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
} else {
|
||||||
|
return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - hasClass':
|
||||||
|
case 'jQuery - addClass':
|
||||||
|
case 'jQuery - removeClass':
|
||||||
|
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
|
||||||
|
case 'jQuery - toggleClass':
|
||||||
|
if (args[0]) {
|
||||||
|
if (args[1]) {
|
||||||
|
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'magic no-argument toggleClass';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - children':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - closest':
|
||||||
|
if (args[1]) {
|
||||||
|
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - next':
|
||||||
|
case 'jQuery - nextAll':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - nextUntil':
|
||||||
|
if (args[0]) {
|
||||||
|
if (args[1]) {
|
||||||
|
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - offsetParent':
|
||||||
|
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
|
||||||
|
case 'jQuery - prev':
|
||||||
|
case 'jQuery - prevAll':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - prevUntil':
|
||||||
|
if (args[0]) {
|
||||||
|
if (args[1]) {
|
||||||
|
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - parent':
|
||||||
|
case 'jQuery - parents':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - parentsUntil':
|
||||||
|
if (args[0]) {
|
||||||
|
if (args[1]) {
|
||||||
|
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - siblings':
|
||||||
|
if (args[0]) {
|
||||||
|
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
|
||||||
|
} else {
|
||||||
|
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jQuery - onDOMReady':
|
||||||
|
return '(function)';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHTML(html) {
|
||||||
|
var entityMap = {
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
"/": '/'
|
||||||
|
};
|
||||||
|
|
||||||
|
return String(html).replace(/[&<>"'\/]/g, function (s) {
|
||||||
|
return entityMap[s];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinArgs(args) {
|
||||||
|
var html = '<b>' + args[0] + '</b>';
|
||||||
|
if (args[1]) {
|
||||||
|
html += ', <b>' + args[1] + '</b>';
|
||||||
|
if (args[2]) {
|
||||||
|
html += ', <b>' + args[2] + '</b>';
|
||||||
|
if (args[3]) {
|
||||||
|
html += ', and more...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringOfObject(str) {
|
||||||
|
return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPureString(str) {
|
||||||
|
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimelineParamsHTML(node, onASingleLine) {
|
||||||
|
if (isJQuery(node)) {
|
||||||
|
return getJQueryHTML(node, onASingleLine);
|
||||||
|
} else {
|
||||||
|
return getNonJQueryHTML(node, onASingleLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBacktraceHTML(backtrace) {
|
||||||
|
var html = '';
|
||||||
|
var parsedBacktrace = parseBacktrace(backtrace);
|
||||||
|
|
||||||
|
if (!parsedBacktrace || parsedBacktrace.length === 0) {
|
||||||
|
html += '<div><div>can\'t find any backtrace :/</div></div>';
|
||||||
|
} else {
|
||||||
|
for (var i = 0 ; i < parsedBacktrace.length ; i++) {
|
||||||
|
html += '<div>';
|
||||||
|
html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
|
||||||
|
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + ':' + parsedBacktrace[i].line + '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBacktrace(str) {
|
||||||
|
if (!str) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var out = [];
|
||||||
|
var splited = str.split(' / ');
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
splited.forEach(function(trace) {
|
||||||
|
var fnName = null, fileAndLine;
|
||||||
|
|
||||||
|
var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
|
||||||
|
if (withFnResult === null) {
|
||||||
|
fileAndLine = trace;
|
||||||
|
} else {
|
||||||
|
fnName = withFnResult[1];
|
||||||
|
fileAndLine = withFnResult[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
|
||||||
|
var filePath = fileAndLineSplit[1];
|
||||||
|
var line = fileAndLineSplit[2];
|
||||||
|
|
||||||
|
out.push({
|
||||||
|
fnName: fnName,
|
||||||
|
filePath: filePath,
|
||||||
|
line: line
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimelineDetailsHTML(node) {
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
|
||||||
|
if (node.warning || node.error) {
|
||||||
|
html += '<div class="icon-warning"></div>';
|
||||||
|
} else {
|
||||||
|
html += '<div class="icon-question"></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
element.append(recursiveHtmlBuilder(scope.tree));
|
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('domElementButton', function() {
|
|
||||||
return {
|
offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
|
||||||
restrict: 'E',
|
|
||||||
scope: {
|
var numberWithCommas = $filter('number');
|
||||||
obj: '='
|
|
||||||
},
|
|
||||||
templateUrl: 'views/domElementButton.html',
|
|
||||||
replace: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.filter('shortenUrl', function() {
|
function getProfilerLineHTML(index, node) {
|
||||||
return function(url, maxLength) {
|
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) {
|
if (!maxLength) {
|
||||||
maxLength = 110;
|
maxLength = 110;
|
||||||
}
|
}
|
||||||
@@ -54,67 +764,56 @@ offendersDirectives.filter('shortenUrl', function() {
|
|||||||
var rightLength = Math.ceil((maxLength - 5) / 2.1);
|
var rightLength = Math.ceil((maxLength - 5) / 2.1);
|
||||||
|
|
||||||
return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
|
return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
|
||||||
};
|
}
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.directive('urlLink', function() {
|
offendersDirectives.filter('shortenUrl', function() {
|
||||||
return {
|
return shortenUrl;
|
||||||
restrict: 'E',
|
});
|
||||||
scope: {
|
|
||||||
url: '=',
|
|
||||||
maxLength: '='
|
|
||||||
},
|
|
||||||
template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
|
|
||||||
replace: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.filter('encodeURIComponent', function() {
|
function getUrlLink(url, maxLength) {
|
||||||
return window.encodeURIComponent;
|
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
|
||||||
});
|
}
|
||||||
|
|
||||||
offendersDirectives.directive('fileAndLine', function() {
|
offendersDirectives.directive('urlLink', function() {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
scope: {
|
scope: {
|
||||||
file: '=',
|
url: '=',
|
||||||
line: '=',
|
maxLength: '='
|
||||||
column: '='
|
},
|
||||||
},
|
template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
|
||||||
template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file"><inline CSS></span> @ {{line}}:{{column}}</span>',
|
replace: true
|
||||||
replace: true
|
};
|
||||||
};
|
});
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.directive('fileAndLineButton', function() {
|
offendersDirectives.filter('encodeURIComponent', function() {
|
||||||
return {
|
return window.encodeURIComponent;
|
||||||
restrict: 'E',
|
});
|
||||||
scope: {
|
|
||||||
file: '=',
|
|
||||||
line: '=',
|
|
||||||
column: '='
|
|
||||||
},
|
|
||||||
template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
|
|
||||||
replace: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
offendersDirectives.directive('offenderButton', function() {
|
offendersDirectives.directive('fileAndLine', function() {
|
||||||
return {
|
return {
|
||||||
restrict: 'C',
|
restrict: 'E',
|
||||||
link: function(scope, element, attrs) {
|
scope: {
|
||||||
|
file: '=',
|
||||||
|
line: '=',
|
||||||
|
column: '='
|
||||||
|
},
|
||||||
|
template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file"><inline CSS></span><span ng-if="line !== null && column !== null"> @ {{line}}:{{column}}</span></span>',
|
||||||
|
replace: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
console.log('initializing touchstart');
|
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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
element.bind('touchstart mouseenter', function(e) {
|
})();
|
||||||
element.addClass('mouseOver');
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
element.bind('touchend mouseleave click', function(e) {
|
|
||||||
element.removeClass('mouseOver');
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -8,7 +8,8 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
|
|||||||
Runs.save({
|
Runs.save({
|
||||||
url: url,
|
url: url,
|
||||||
waitForResponse: false,
|
waitForResponse: false,
|
||||||
screenshot: true
|
screenshot: true,
|
||||||
|
jsTimeline: true
|
||||||
}, function(data) {
|
}, function(data) {
|
||||||
$location.path('/queue/' + data.runId);
|
$location.path('/queue/' + data.runId);
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
border-bottom-left-radius: 0.4em;
|
border-bottom-left-radius: 0.4em;
|
||||||
border-bottom-right-radius: 0.4em;
|
border-bottom-right-radius: 0.4em;
|
||||||
border-top: 1px solid #999;
|
border-top: 1px solid #999;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domTree {
|
.domTree {
|
||||||
@@ -140,7 +141,7 @@
|
|||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.opens.mouseOver {
|
&.opens:hover {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
background: #ffe0cc;
|
background: #ffe0cc;
|
||||||
|
|||||||
+102
-17
@@ -14,6 +14,23 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selectScript {
|
||||||
|
padding-bottom: 2em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
select {
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
margin: 2em 0 5em;
|
margin: 2em 0 5em;
|
||||||
}
|
}
|
||||||
@@ -49,6 +66,10 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline div.interval:hover {
|
.timeline div.interval:hover {
|
||||||
background: #9C4274;
|
background: #9C4274;
|
||||||
@@ -141,22 +162,6 @@
|
|||||||
text-align: left;
|
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 {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -189,14 +194,82 @@ input.textFilter {
|
|||||||
background: #f1c40f;
|
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 {
|
.table > div > .index {
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table > div > .type {
|
.table > div > .type {
|
||||||
white-space:nowrap;
|
white-space:nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table .children {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .child {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
|
||||||
|
> .child {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "↳";
|
||||||
|
}
|
||||||
|
|
||||||
|
.childArgs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:hover {
|
||||||
|
background: #EBD8E2;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.childArgs {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
padding: 0 1em 0 2em;
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
background: #EBD8E2;
|
||||||
|
line-height: 1.3em;
|
||||||
|
height: 1.3em;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .showingDetails .child span:hover {
|
||||||
|
background: inherit;
|
||||||
|
|
||||||
|
.childArgs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.table > div > .value {
|
.table > div > .value {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -215,6 +288,7 @@ input.textFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detailsOverlay {
|
.detailsOverlay {
|
||||||
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 3em;
|
right: 3em;
|
||||||
top: -3em;
|
top: -3em;
|
||||||
@@ -224,13 +298,16 @@ input.textFilter {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border: 2px solid #f1c40f;
|
border: 2px solid #f1c40f;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 1024px) {
|
@media screen and (max-width: 1024px) {
|
||||||
.detailsOverlay {
|
.detailsOverlay {
|
||||||
width: 25em;
|
width: 25em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.showDetails .detailsOverlay {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.detailsOverlay .closeBtn {
|
.detailsOverlay .closeBtn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5em;
|
top: 0.5em;
|
||||||
@@ -268,4 +345,12 @@ input.textFilter {
|
|||||||
.execution .icon-warning {
|
.execution .icon-warning {
|
||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.warningsFilterOn {
|
||||||
|
> div {
|
||||||
|
display: none;
|
||||||
|
&.warning {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<script src="/bower_components/angular-route/angular-route.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-resource/angular-resource.min.js"></script>
|
||||||
<script src="/bower_components/angular-sanitize/angular-sanitize.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/app.js"></script>
|
||||||
<script src="/js/controllers/indexCtrl.js"></script>
|
<script src="/js/controllers/indexCtrl.js"></script>
|
||||||
<script src="/js/controllers/dashboardCtrl.js"></script>
|
<script src="/js/controllers/dashboardCtrl.js"></script>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<div class="offenderButton" ng-class="{opens: obj.tree}">
|
|
||||||
<span ng-if="obj.type == 'html' || obj.type == 'body' || obj.type == 'head' || obj.type == 'window' || obj.type == 'document' || obj.type == 'fragment'"><b>{{obj.type}}</b></span>
|
|
||||||
<span ng-if="obj.type == 'domElement'">DOM element <b>{{obj.element}}</b></span>
|
|
||||||
<span ng-if="obj.type == 'fragmentElement'">Fragment element <b>{{obj.element}}</b></span>
|
|
||||||
<span ng-if="obj.type == 'createdElement'">Created element <b>{{obj.element}}</b></span>
|
|
||||||
<dom-tree ng-if="obj.tree" tree="obj.tree"></dom-tree>
|
|
||||||
</div>
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ng-if="notFound == true">
|
<div ng-if="notFound == true">
|
||||||
<div class="status">Error 404 (test not found)</div>
|
<div class="status">Error 404 (test not found)</div>
|
||||||
<p class="statusSubMessage">The server probably rebooted. We are very sorry about that, please try to launch the test again.</p>
|
<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>
|
<a class="linkButton" href="/">New test</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,6 +62,19 @@
|
|||||||
</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'">
|
<div ng-if="policyName === 'cssParsingErrors'">
|
||||||
<b>{{offender.error}}</b>
|
<b>{{offender.error}}</b>
|
||||||
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
|
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||||
<div class="execution board">
|
<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>
|
<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>
|
<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,
|
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
|
||||||
domComplete: $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 class="tooltip detailsOverlay">
|
||||||
<div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
|
<div>
|
||||||
|
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,15 +63,8 @@
|
|||||||
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
|
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
|
||||||
<label for="warningsFilterOn">Show warnings only</label>
|
<label for="warningsFilterOn">Show warnings only</label>
|
||||||
</div>
|
</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>
|
||||||
<div class="table">
|
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
|
||||||
|
|
||||||
<toto data-info="mavariable"></toto>
|
|
||||||
<div class="headers">
|
<div class="headers">
|
||||||
<div><!-- index --></div>
|
<div><!-- index --></div>
|
||||||
<div>Type</div>
|
<div>Type</div>
|
||||||
@@ -70,74 +72,9 @@
|
|||||||
<div><!-- details --></div>
|
<div><!-- details --></div>
|
||||||
<div>Timestamp</div>
|
<div>Timestamp</div>
|
||||||
</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>
|
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
|
||||||
<div class="type">{{node.data.type}}</div>
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
|
||||||
|
|||||||
@@ -242,7 +242,49 @@ var policies = {
|
|||||||
"isOkThreshold": 0,
|
"isOkThreshold": 0,
|
||||||
"isBadThreshold": 10,
|
"isBadThreshold": 10,
|
||||||
"isAbnormalThreshold": 20,
|
"isAbnormalThreshold": 20,
|
||||||
"hasOffenders": false
|
"hasOffenders": true,
|
||||||
|
"offendersTransformFn": function(offenders) {
|
||||||
|
return {
|
||||||
|
count: offenders.length,
|
||||||
|
list: offenders.map(function(offender) {
|
||||||
|
var parts = /^document.write(ln)?\(\) used from (.*)$/.exec(offender);
|
||||||
|
|
||||||
|
if (parts) {
|
||||||
|
|
||||||
|
var writeFn = 'document.write' + (parts[1] || '');
|
||||||
|
|
||||||
|
var methodParts = /^([^\s]+) \((.+):(\d+)\)$/.exec(parts[2]);
|
||||||
|
if (methodParts) {
|
||||||
|
return {
|
||||||
|
writeFn: writeFn,
|
||||||
|
from: {
|
||||||
|
functionName: methodParts[1],
|
||||||
|
file: methodParts[2],
|
||||||
|
line: methodParts[3]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
var noMethodParts = /^(.+):(\d+)$/.exec(parts[2]);
|
||||||
|
|
||||||
|
if (noMethodParts) {
|
||||||
|
return {
|
||||||
|
writeFn: writeFn,
|
||||||
|
from: {
|
||||||
|
file: noMethodParts[1],
|
||||||
|
line: noMethodParts[2]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('documentWriteCalls offenders transform function error with "%s"', offender);
|
||||||
|
return {
|
||||||
|
parseError: offender
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"consoleMessages": {
|
"consoleMessages": {
|
||||||
"tool": "phantomas",
|
"tool": "phantomas",
|
||||||
|
|||||||
@@ -91,9 +91,10 @@
|
|||||||
"network": {
|
"network": {
|
||||||
"label": "Network",
|
"label": "Network",
|
||||||
"policies": {
|
"policies": {
|
||||||
"notFound": 3,
|
"notFound": 2,
|
||||||
"closedConnections": 3,
|
"closedConnections": 2,
|
||||||
"multipleRequests": 3,
|
"multipleRequests": 2,
|
||||||
|
"cachingNotSpecified": 1,
|
||||||
"cachingDisabled": 1,
|
"cachingDisabled": 1,
|
||||||
"cachingTooShort": 1,
|
"cachingTooShort": 1,
|
||||||
"domains": 1
|
"domains": 1
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ var OffendersHelpers = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.domPathToDomElementObj = function(domPath) {
|
this.domPathToDomElementObj = function(domPath) {
|
||||||
|
|
||||||
|
if (typeof domPath === 'boolean') {
|
||||||
|
return {
|
||||||
|
// Not a normal element path
|
||||||
|
type: 'notAnElement',
|
||||||
|
element: domPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var domArray = this.domPathToArray(domPath);
|
var domArray = this.domPathToArray(domPath);
|
||||||
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
|
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ var Runner = function(params) {
|
|||||||
data.toolsResults.phantomas = phantomasResults;
|
data.toolsResults.phantomas = phantomasResults;
|
||||||
|
|
||||||
// Treat the JS Execution Tree from offenders
|
// Treat the JS Execution Tree from offenders
|
||||||
data.javascriptExecutionTree = jsExecutionTransformer.transform(data);
|
data = jsExecutionTransformer.transform(data);
|
||||||
|
|
||||||
// Other tools go here
|
// Other tools go here
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ var screenshotHandler = function() {
|
|||||||
this.getScreenshotTempFile = function() {
|
this.getScreenshotTempFile = function() {
|
||||||
|
|
||||||
var screenshotTmpFolder = new tmp.Dir();
|
var screenshotTmpFolder = new tmp.Dir();
|
||||||
var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.jpg');
|
var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.png');
|
||||||
var that = this;
|
var that = this;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ var ApiController = function(app) {
|
|||||||
url: req.body.url,
|
url: req.body.url,
|
||||||
waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
|
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
|
screenshot: req.body.screenshot || false,
|
||||||
|
jsTimeline: req.body.jsTimeline || false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,7 +65,8 @@ var ApiController = function(app) {
|
|||||||
debug('Launching test %s on %s', run.runId, run.params.url);
|
debug('Launching test %s on %s', run.runId, run.params.url);
|
||||||
|
|
||||||
var runOptions = {
|
var runOptions = {
|
||||||
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false
|
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
|
||||||
|
jsDeepAnalysis: run.params.jsTimeline
|
||||||
};
|
};
|
||||||
|
|
||||||
return ylt(run.params.url, runOptions);
|
return ylt(run.params.url, runOptions);
|
||||||
@@ -107,7 +109,16 @@ var ApiController = function(app) {
|
|||||||
|
|
||||||
// Save results
|
// Save results
|
||||||
.then(function() {
|
.then(function() {
|
||||||
|
// Remove uneeded temp screenshot path
|
||||||
delete data.params.options.screenshot;
|
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);
|
return resultsDatastore.saveResult(data);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
var debug = require('debug')('ylt:jsExecutionTransformer');
|
var debug = require('debug')('ylt:jsExecutionTransformer');
|
||||||
|
|
||||||
|
var offendersHelpers = require('../offendersHelpers');
|
||||||
|
|
||||||
var jsExecutionTransformer = function() {
|
var jsExecutionTransformer = function() {
|
||||||
|
|
||||||
this.transform = function(data) {
|
this.transform = function(data) {
|
||||||
var javascriptExecutionTree = {};
|
var javascriptExecutionTree = {};
|
||||||
|
|
||||||
|
var metrics = {
|
||||||
|
domManipulations: 0,
|
||||||
|
queriesWithoutResults: 0,
|
||||||
|
jQueryCalls: 0,
|
||||||
|
jQueryCallsOnEmptyObject: 0
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
debug('Starting JS execution transformation');
|
debug('Starting JS execution transformation');
|
||||||
|
|
||||||
@@ -46,6 +56,22 @@ var jsExecutionTransformer = function() {
|
|||||||
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
|
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
|
||||||
break;
|
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.type === 'getComputedStyle') {
|
||||||
|
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.data.type === 'insertBefore') {
|
||||||
|
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +81,22 @@ var jsExecutionTransformer = function() {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
return javascriptExecutionTree;
|
data.javascriptExecutionTree = javascriptExecutionTree;
|
||||||
|
data.toolsResults.jsExecutionTransformer = {
|
||||||
|
metrics: metrics
|
||||||
|
};
|
||||||
|
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function treeRecursiveParser(node, fn) {
|
||||||
|
if (node.children) {
|
||||||
|
node.children.forEach(function(child) {
|
||||||
|
treeRecursiveParser(child, fn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn(node);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = new jsExecutionTransformer();
|
module.exports = new jsExecutionTransformer();
|
||||||
@@ -36,7 +36,7 @@ exports.module = function(phantomas) {
|
|||||||
phantomas.log('Overwritting phantomas spy function');
|
phantomas.log('Overwritting phantomas spy function');
|
||||||
|
|
||||||
function spy(obj, fn, callbackBefore, callbackAfter) {
|
function spy(obj, fn, callbackBefore, callbackAfter) {
|
||||||
var origFn = obj[fn];
|
var origFn = obj && obj[fn];
|
||||||
|
|
||||||
if (typeof origFn !== 'function') {
|
if (typeof origFn !== 'function') {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
/**
|
|
||||||
* Analyzes HTTP caching headers
|
|
||||||
*
|
|
||||||
* @see https://developers.google.com/speed/docs/best-practices/caching
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.version = '0.2.a';
|
|
||||||
|
|
||||||
exports.module = function(phantomas) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var cacheControlRegExp = /max-age=(\d+)/;
|
|
||||||
|
|
||||||
function getCachingTime(url, headers) {
|
|
||||||
// false means "no caching"
|
|
||||||
var ttl = false,
|
|
||||||
headerName,
|
|
||||||
now = new Date(),
|
|
||||||
headerDate;
|
|
||||||
|
|
||||||
for (headerName in headers) {
|
|
||||||
var value = headers[headerName];
|
|
||||||
|
|
||||||
switch (headerName.toLowerCase()) {
|
|
||||||
// parse max-age=...
|
|
||||||
//
|
|
||||||
// max-age=2592000
|
|
||||||
// public, max-age=300, must-revalidate
|
|
||||||
case 'cache-control':
|
|
||||||
var matches = value.match(cacheControlRegExp);
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
ttl = parseInt(matches[1], 10);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// catch Expires and Pragma headers
|
|
||||||
case 'expires':
|
|
||||||
case 'pragma':
|
|
||||||
// and Varnish specific headers
|
|
||||||
case 'x-pass-expires':
|
|
||||||
case 'x-pass-cache-control':
|
|
||||||
phantomas.incrMetric('oldCachingHeaders'); // @desc number of responses with old, HTTP 1.0 caching headers (Expires and Pragma)
|
|
||||||
phantomas.addOffender('oldCachingHeaders', url + ' - ' + headerName + ': ' + value);
|
|
||||||
headerDate = Date.parse(value);
|
|
||||||
if (headerDate) ttl = Math.round((headerDate - now) / 1000);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log(JSON.stringify(headers)); console.log("TTL: " + ttl + ' s');
|
|
||||||
|
|
||||||
return ttl;
|
|
||||||
}
|
|
||||||
|
|
||||||
phantomas.setMetric('cachingNotSpecified'); // @desc number of responses with no caching header sent (no Cache-Control header)
|
|
||||||
phantomas.setMetric('cachingTooShort'); // @desc number of responses with too short (less than a week) caching time
|
|
||||||
phantomas.setMetric('cachingDisabled'); // @desc number of responses with caching disabled (max-age=0)
|
|
||||||
|
|
||||||
phantomas.setMetric('oldCachingHeaders');
|
|
||||||
|
|
||||||
phantomas.on('recv', function(entry, res) {
|
|
||||||
var ttl = getCachingTime(entry.url, entry.headers);
|
|
||||||
|
|
||||||
// static assets
|
|
||||||
if (entry.isImage || entry.isJS || entry.isCSS) {
|
|
||||||
if (ttl === false) {
|
|
||||||
phantomas.incrMetric('cachingNotSpecified');
|
|
||||||
phantomas.addOffender('cachingNotSpecified', entry.url);
|
|
||||||
} else if (ttl <= 0) {
|
|
||||||
phantomas.incrMetric('cachingDisabled');
|
|
||||||
phantomas.addOffender('cachingDisabled', entry.url);
|
|
||||||
} else if (ttl < 7 * 86400) {
|
|
||||||
phantomas.incrMetric('cachingTooShort');
|
|
||||||
phantomas.addOffender('cachingTooShort', entry.url + ' cached for ' + ttl + ' s');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -33,9 +33,6 @@ exports.module = function(phantomas) {
|
|||||||
phantomas.enterContext({
|
phantomas.enterContext({
|
||||||
type: 'getElementById',
|
type: 'getElementById',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
|
||||||
domElement: '#document'
|
|
||||||
},
|
|
||||||
arguments: ['#' + id]
|
arguments: ['#' + id]
|
||||||
},
|
},
|
||||||
backtrace: phantomas.getBacktrace()
|
backtrace: phantomas.getBacktrace()
|
||||||
@@ -65,7 +62,8 @@ exports.module = function(phantomas) {
|
|||||||
type: 'getElementsByClassName',
|
type: 'getElementsByClassName',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
domElement: context
|
length: 1,
|
||||||
|
elements: [context]
|
||||||
},
|
},
|
||||||
arguments: ['.' + className]
|
arguments: ['.' + className]
|
||||||
},
|
},
|
||||||
@@ -103,7 +101,8 @@ exports.module = function(phantomas) {
|
|||||||
type: 'getElementsByTagName',
|
type: 'getElementsByTagName',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
domElement: context
|
length: 1,
|
||||||
|
elements: [context]
|
||||||
},
|
},
|
||||||
arguments: [tagName]
|
arguments: [tagName]
|
||||||
},
|
},
|
||||||
@@ -145,7 +144,8 @@ exports.module = function(phantomas) {
|
|||||||
type: 'querySelector',
|
type: 'querySelector',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
domElement: context
|
length: 1,
|
||||||
|
elements: [context]
|
||||||
},
|
},
|
||||||
arguments: [selector]
|
arguments: [selector]
|
||||||
},
|
},
|
||||||
@@ -177,7 +177,8 @@ exports.module = function(phantomas) {
|
|||||||
type: 'querySelectorAll',
|
type: 'querySelectorAll',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
domElement: context
|
length: 1,
|
||||||
|
elements: [context]
|
||||||
},
|
},
|
||||||
arguments: [selector]
|
arguments: [selector]
|
||||||
},
|
},
|
||||||
@@ -237,7 +238,8 @@ exports.module = function(phantomas) {
|
|||||||
type: 'appendChild',
|
type: 'appendChild',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
domElement: context
|
length: 1,
|
||||||
|
elements: [context]
|
||||||
},
|
},
|
||||||
arguments: [appended]
|
arguments: [appended]
|
||||||
},
|
},
|
||||||
@@ -245,20 +247,25 @@ exports.module = function(phantomas) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertBeforeSpyBefore(child) {
|
function insertBeforeSpyBefore(child, refElement) {
|
||||||
/*jshint validthis: true */
|
/*jshint validthis: true */
|
||||||
|
|
||||||
var context = phantomas.getDOMPath(this);
|
var context = phantomas.getDOMPath(this);
|
||||||
var appended = phantomas.getDOMPath(child);
|
var appended = phantomas.getDOMPath(child);
|
||||||
|
var referent = phantomas.getDOMPath(refElement);
|
||||||
appendChild(child, this, context, appended);
|
appendChild(child, this, context, appended);
|
||||||
|
|
||||||
phantomas.enterContext({
|
phantomas.enterContext({
|
||||||
type: 'insertBefore',
|
type: 'insertBefore',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
domElement: context
|
length: 1,
|
||||||
|
elements: [context]
|
||||||
},
|
},
|
||||||
arguments: [appended]
|
arguments: [
|
||||||
|
appended,
|
||||||
|
referent
|
||||||
|
]
|
||||||
},
|
},
|
||||||
backtrace: phantomas.getBacktrace()
|
backtrace: phantomas.getBacktrace()
|
||||||
});
|
});
|
||||||
@@ -270,6 +277,68 @@ exports.module = function(phantomas) {
|
|||||||
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
|
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
|
||||||
phantomas.leaveContext();
|
phantomas.leaveContext();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
phantomas.spy(Document.prototype, 'createElement', function(tagName) {
|
||||||
|
|
||||||
|
phantomas.enterContext({
|
||||||
|
type: 'createElement',
|
||||||
|
callDetails: {
|
||||||
|
arguments: [tagName]
|
||||||
|
},
|
||||||
|
backtrace: phantomas.getBacktrace()
|
||||||
|
});
|
||||||
|
|
||||||
|
}, function(result, args) {
|
||||||
|
phantomas.leaveContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
phantomas.spy(Document.prototype, 'createTextNode', function(text) {
|
||||||
|
|
||||||
|
phantomas.enterContext({
|
||||||
|
type: 'createTextNode',
|
||||||
|
callDetails: {
|
||||||
|
arguments: [text]
|
||||||
|
},
|
||||||
|
backtrace: phantomas.getBacktrace()
|
||||||
|
});
|
||||||
|
|
||||||
|
}, function(result, args) {
|
||||||
|
phantomas.leaveContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
phantomas.spy(Document.prototype, 'createDocumentFragment', function() {
|
||||||
|
|
||||||
|
phantomas.enterContext({
|
||||||
|
type: 'createDocumentFragment',
|
||||||
|
callDetails: {
|
||||||
|
arguments: []
|
||||||
|
},
|
||||||
|
backtrace: phantomas.getBacktrace()
|
||||||
|
});
|
||||||
|
|
||||||
|
}, function(result, args) {
|
||||||
|
phantomas.leaveContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) {
|
||||||
|
var target = phantomas.getDOMPath(element);
|
||||||
|
|
||||||
|
phantomas.enterContext({
|
||||||
|
type: 'getComputedStyle',
|
||||||
|
callDetails: {
|
||||||
|
arguments: [target, pseudoElement]
|
||||||
|
},
|
||||||
|
backtrace: phantomas.getBacktrace()
|
||||||
|
});
|
||||||
|
|
||||||
|
}, function(result, args) {
|
||||||
|
phantomas.leaveContext();
|
||||||
|
});
|
||||||
|
|
||||||
})(window.__phantomas);
|
})(window.__phantomas);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ exports.module = function(phantomas) {
|
|||||||
type: 'addEventListener',
|
type: 'addEventListener',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
domElement: path
|
length: 1,
|
||||||
|
elements: [path]
|
||||||
},
|
},
|
||||||
arguments: [eventType]
|
arguments: [eventType]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ exports.module = function(phantomas) {
|
|||||||
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
|
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
|
||||||
phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad 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('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
|
||||||
//phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
|
phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
|
||||||
|
|
||||||
var jQueryFunctions = [
|
var jQueryFunctions = [
|
||||||
// DOM manipulations
|
// DOM manipulations
|
||||||
@@ -55,7 +55,20 @@ exports.module = function(phantomas) {
|
|||||||
'scrollLeft',
|
'scrollLeft',
|
||||||
'scrollTop',
|
'scrollTop',
|
||||||
|
|
||||||
// generic events
|
// Animations
|
||||||
|
'hide',
|
||||||
|
'show',
|
||||||
|
'toggle',
|
||||||
|
'animate',
|
||||||
|
'fadeIn',
|
||||||
|
'fadeOut',
|
||||||
|
'fadeTo',
|
||||||
|
'fadeToggle',
|
||||||
|
'slideDown',
|
||||||
|
'slideUp',
|
||||||
|
'slideToggle',
|
||||||
|
|
||||||
|
// Generic events
|
||||||
'on',
|
'on',
|
||||||
'off',
|
'off',
|
||||||
'live',
|
'live',
|
||||||
@@ -66,7 +79,7 @@ exports.module = function(phantomas) {
|
|||||||
'bind',
|
'bind',
|
||||||
'unbind',
|
'unbind',
|
||||||
|
|
||||||
// more events
|
// More events
|
||||||
'blur',
|
'blur',
|
||||||
'change',
|
'change',
|
||||||
'click',
|
'click',
|
||||||
@@ -91,10 +104,9 @@ exports.module = function(phantomas) {
|
|||||||
'scroll',
|
'scroll',
|
||||||
'select',
|
'select',
|
||||||
'submit',
|
'submit',
|
||||||
'toggle',
|
|
||||||
'unload',
|
'unload',
|
||||||
|
|
||||||
// attributes
|
// Attributes
|
||||||
'attr',
|
'attr',
|
||||||
'prop',
|
'prop',
|
||||||
'removeAttr',
|
'removeAttr',
|
||||||
@@ -103,12 +115,31 @@ exports.module = function(phantomas) {
|
|||||||
'hasClass',
|
'hasClass',
|
||||||
'addClass',
|
'addClass',
|
||||||
'removeClass',
|
'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
|
// spy calls to jQuery functions
|
||||||
phantomas.once('init', function() {
|
phantomas.once('init', function() {
|
||||||
phantomas.evaluate(function(jQueryFunctions) {
|
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
|
||||||
(function(phantomas) {
|
(function(phantomas) {
|
||||||
var jQuery;
|
var jQuery;
|
||||||
var oldJQuery;
|
var oldJQuery;
|
||||||
@@ -166,11 +197,11 @@ exports.module = function(phantomas) {
|
|||||||
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
|
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
|
||||||
|
|
||||||
phantomas.enterContext({
|
phantomas.enterContext({
|
||||||
type: 'jQuery - find',
|
type: 'jQuery - Sizzle call',
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
length: this.length,
|
length: 1,
|
||||||
firstElementPath: phantomas.getDOMPath(context)
|
elements: [phantomas.getDOMPath(context)]
|
||||||
},
|
},
|
||||||
arguments: [selector]
|
arguments: [selector]
|
||||||
},
|
},
|
||||||
@@ -184,7 +215,37 @@ exports.module = function(phantomas) {
|
|||||||
phantomas.leaveContext(moreData);
|
phantomas.leaveContext(moreData);
|
||||||
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
|
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
|
||||||
|
|
||||||
/*if (!jQuery.event) {
|
|
||||||
|
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) {
|
phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
|
||||||
var path = phantomas.getDOMPath(elem),
|
var path = phantomas.getDOMPath(elem),
|
||||||
type = ev.type || ev;
|
type = ev.type || ev;
|
||||||
@@ -194,27 +255,18 @@ exports.module = function(phantomas) {
|
|||||||
phantomas.incrMetric('jQueryEventTriggers');
|
phantomas.incrMetric('jQueryEventTriggers');
|
||||||
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
|
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
|
||||||
});
|
});
|
||||||
}*/
|
}
|
||||||
|
|
||||||
// jQuery events bound to window' onLoad event (#451)
|
// jQuery events bound to window' onLoad event (#451)
|
||||||
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
|
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
|
||||||
if ((eventName === 'load') && (this[0] === window)) {
|
if ((eventName === 'load') && (this[0] === window)) {
|
||||||
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
|
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
|
||||||
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
|
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
|
||||||
|
|
||||||
phantomas.pushContext({
|
|
||||||
type: 'jQuery - windowOnLoad',
|
|
||||||
callDetails: {
|
|
||||||
arguments: [func]
|
|
||||||
},
|
|
||||||
backtrace: phantomas.getBacktrace()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add spys on many jQuery functions
|
// Add spys on many jQuery functions
|
||||||
jQueryFunctions.forEach(function(functionName) {
|
jQueryFunctions.forEach(function(functionName) {
|
||||||
var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
|
|
||||||
|
|
||||||
phantomas.spy(jQuery.fn, functionName, function(args) {
|
phantomas.spy(jQuery.fn, functionName, function(args) {
|
||||||
|
|
||||||
@@ -222,6 +274,11 @@ exports.module = function(phantomas) {
|
|||||||
args = [].slice.call(arguments);
|
args = [].slice.call(arguments);
|
||||||
args.forEach(function(arg, index) {
|
args.forEach(function(arg, index) {
|
||||||
|
|
||||||
|
|
||||||
|
if (arg instanceof Array) {
|
||||||
|
arg = '[Array]';
|
||||||
|
}
|
||||||
|
|
||||||
if (arg instanceof Object) {
|
if (arg instanceof Object) {
|
||||||
|
|
||||||
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
|
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
|
||||||
@@ -274,13 +331,17 @@ exports.module = function(phantomas) {
|
|||||||
args[index] = arg;
|
args[index] = arg;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var elements = [];
|
||||||
|
for (var i = 0 ; i < this.length ; i++) {
|
||||||
|
elements.push(phantomas.getDOMPath(this[i]));
|
||||||
|
}
|
||||||
|
|
||||||
phantomas.enterContext({
|
phantomas.enterContext({
|
||||||
type: 'jQuery - ' + functionName,
|
type: 'jQuery - ' + functionName,
|
||||||
callDetails: {
|
callDetails: {
|
||||||
context: {
|
context: {
|
||||||
length: this.length,
|
length: this.length,
|
||||||
firstElementPath: phantomas.getDOMPath(this[0])
|
elements: elements
|
||||||
},
|
},
|
||||||
arguments: args
|
arguments: args
|
||||||
},
|
},
|
||||||
@@ -288,12 +349,19 @@ exports.module = function(phantomas) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
}, function(result) {
|
}, function(result) {
|
||||||
phantomas.leaveContext();
|
if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
|
||||||
}) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)');
|
var moreData = {
|
||||||
|
resultsNumber : (result && result.length) ? result.length : 0
|
||||||
|
};
|
||||||
|
phantomas.leaveContext(moreData);
|
||||||
|
} else {
|
||||||
|
phantomas.leaveContext();
|
||||||
|
}
|
||||||
|
}) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})(window.__phantomas);
|
})(window.__phantomas);
|
||||||
}, jQueryFunctions);
|
}, jQueryFunctions, jQueryTraversalFunctions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+25
-13
@@ -4,44 +4,56 @@
|
|||||||
* @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
|
* @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.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
|
||||||
* @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
|
* @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 */
|
/* global document: true, window: true */
|
||||||
|
|
||||||
exports.version = '0.1.a';
|
exports.version = '0.2';
|
||||||
|
|
||||||
exports.module = function(phantomas) {
|
exports.module = function(phantomas) {
|
||||||
'use strict';
|
'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
|
// spy calls to eval only when requested (issue #467)
|
||||||
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval)
|
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.once('init', function() {
|
||||||
phantomas.evaluate(function() {
|
phantomas.evaluate(function(spyEval) {
|
||||||
(function(phantomas) {
|
(function(phantomas) {
|
||||||
function report(msg, caller, backtrace, metric) {
|
function report(msg, caller, backtrace, metric) {
|
||||||
phantomas.log(msg + ': from ' + caller + '!');
|
phantomas.log(msg + ': from ' + caller + '!');
|
||||||
phantomas.log('Backtrace: ' + backtrace);
|
phantomas.log('Backtrace: ' + backtrace);
|
||||||
|
|
||||||
phantomas.incrMetric(metric);
|
phantomas.incrMetric(metric);
|
||||||
|
phantomas.addOffender(metric, "%s from %s", msg, caller);
|
||||||
}
|
}
|
||||||
|
|
||||||
// spy calls to eval()
|
// spy calls to eval()
|
||||||
/*phantomas.spy(window, 'eval', function(code) {
|
if (spyEval) {
|
||||||
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
phantomas.spy(window, 'eval', function(code) {
|
||||||
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
|
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
|
// 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;
|
if (typeof fn !== 'string') return;
|
||||||
|
|
||||||
report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
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;
|
if (typeof fn !== 'string') return;
|
||||||
|
|
||||||
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
|
||||||
});*/
|
});
|
||||||
|
|
||||||
// spy document.write(ln)
|
// spy document.write(ln)
|
||||||
phantomas.spy(document, 'write', function(arg) {
|
phantomas.spy(document, 'write', function(arg) {
|
||||||
@@ -52,6 +64,6 @@ exports.module = function(phantomas) {
|
|||||||
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
|
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
|
||||||
});
|
});
|
||||||
})(window.__phantomas);
|
})(window.__phantomas);
|
||||||
});
|
}, spyEval);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,14 +29,12 @@ var PhantomasWrapper = function() {
|
|||||||
'js-deep-analysis': task.options.jsDeepAnalysis || false,
|
'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',
|
'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,
|
'screenshot': task.options.screenshot || false,
|
||||||
'viewport': '1366x768',
|
|
||||||
|
|
||||||
// Mandatory
|
// Mandatory
|
||||||
'reporter': 'json:pretty',
|
'reporter': 'json:pretty',
|
||||||
'analyze-css': true,
|
'analyze-css': true,
|
||||||
'skip-modules': [
|
'skip-modules': [
|
||||||
'blockDomains', // not needed
|
'blockDomains', // not needed
|
||||||
'caching', // overriden
|
|
||||||
'domMutations', // not compatible with webkit
|
'domMutations', // not compatible with webkit
|
||||||
'domQueries', // overriden
|
'domQueries', // overriden
|
||||||
'eventListeners', // overridden
|
'eventListeners', // overridden
|
||||||
|
|||||||
+22
-22
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yellowlabtools",
|
"name": "yellowlabtools",
|
||||||
"version": "1.2.0",
|
"version": "1.3.2",
|
||||||
"description": "Online tool to audit a webpage for performance and front-end quality issues",
|
"description": "Online tool to audit a webpage for performance and front-end quality issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -12,45 +12,45 @@
|
|||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "~0.9.0",
|
"async": "~0.9.0",
|
||||||
"body-parser": "~1.10.0",
|
"body-parser": "~1.12.1",
|
||||||
"compression": "~1.2.2",
|
"compression": "~1.4.3",
|
||||||
"cors": "^2.5.2",
|
"cors": "^2.5.3",
|
||||||
"debug": "~2.1.0",
|
"debug": "~2.1.3",
|
||||||
"express": "~4.10.6",
|
"express": "~4.12.2",
|
||||||
"lwip": "0.0.6",
|
"lwip": "0.0.6",
|
||||||
"meow": "^3.0.0",
|
"meow": "^3.0.0",
|
||||||
"phantomas": "1.9.0",
|
"phantomas": "1.10.0",
|
||||||
"ps-node": "0.0.3",
|
"ps-node": "0.0.3",
|
||||||
"q": "~1.1.2",
|
"q": "~1.1.2",
|
||||||
"rimraf": "~2.2.8",
|
"rimraf": "~2.3.2",
|
||||||
"temporary": "0.0.8"
|
"temporary": "0.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^1.10.0",
|
"chai": "^2.1.2",
|
||||||
"grunt": "^0.4.5",
|
"grunt": "^0.4.5",
|
||||||
"grunt-blanket": "^0.0.8",
|
"grunt-blanket": "^0.0.8",
|
||||||
"grunt-contrib-clean": "^0.6.0",
|
"grunt-contrib-clean": "^0.6.0",
|
||||||
"grunt-contrib-concat": "^0.5.0",
|
"grunt-contrib-concat": "^0.5.1",
|
||||||
"grunt-contrib-copy": "^0.7.0",
|
"grunt-contrib-copy": "^0.8.0",
|
||||||
"grunt-contrib-cssmin": "^0.11.0",
|
"grunt-contrib-cssmin": "^0.12.2",
|
||||||
"grunt-contrib-htmlmin": "^0.3.0",
|
"grunt-contrib-htmlmin": "^0.4.0",
|
||||||
"grunt-contrib-jshint": "^0.10.0",
|
"grunt-contrib-jshint": "^0.11.0",
|
||||||
"grunt-contrib-less": "^0.12.0",
|
"grunt-contrib-less": "^1.0.0",
|
||||||
"grunt-contrib-uglify": "^0.7.0",
|
"grunt-contrib-uglify": "^0.8.0",
|
||||||
"grunt-env": "^0.4.2",
|
"grunt-env": "^0.4.4",
|
||||||
"grunt-express": "^1.4.1",
|
"grunt-express": "^1.4.1",
|
||||||
"grunt-filerev": "^2.1.2",
|
"grunt-filerev": "^2.1.2",
|
||||||
"grunt-fontsmith": "^0.9.1",
|
"grunt-fontsmith": "^0.9.1",
|
||||||
"grunt-inline-angular-templates": "^0.1.5",
|
"grunt-inline-angular-templates": "^0.1.5",
|
||||||
"grunt-line-remover": "^0.0.2",
|
"grunt-line-remover": "^0.0.2",
|
||||||
"grunt-mocha-test": "^0.12.4",
|
"grunt-mocha-test": "^0.12.7",
|
||||||
"grunt-replace": "^0.8.0",
|
"grunt-replace": "^0.8.0",
|
||||||
"grunt-usemin": "^3.0.0",
|
"grunt-usemin": "^3.0.0",
|
||||||
"matchdep": "^0.3.0",
|
"matchdep": "^0.3.0",
|
||||||
"mocha": "^2.1.0",
|
"mocha": "^2.2.1",
|
||||||
"request": "^2.51.0",
|
"request": "^2.53.0",
|
||||||
"sinon": "^1.12.1",
|
"sinon": "^1.14.0",
|
||||||
"sinon-chai": "^2.6.0"
|
"sinon-chai": "^2.7.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "grunt test"
|
"test": "grunt test"
|
||||||
|
|||||||
@@ -30,4 +30,4 @@ grunt build
|
|||||||
# Start the server
|
# Start the server
|
||||||
rm server_config/settings.json
|
rm server_config/settings.json
|
||||||
cp server_config/settings-prod.json server_config/settings.json
|
cp server_config/settings-prod.json server_config/settings.json
|
||||||
NODE_ENV=production forever start -c "node --stack-size=65500" bin/server.js
|
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
|
||||||
@@ -23,4 +23,4 @@ npm install phantomjs -g
|
|||||||
grunt build
|
grunt build
|
||||||
|
|
||||||
# Restart the server
|
# 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
|
||||||
+5
-1
@@ -159,7 +159,10 @@ describe('api', function() {
|
|||||||
body.should.have.a.property('scoreProfiles').that.is.an('object');
|
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('rules').that.is.an('object');
|
||||||
body.should.have.a.property('toolsResults').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.should.have.a.property('javascriptExecutionTree').that.is.an('object');
|
||||||
|
body.javascriptExecutionTree.should.deep.equal({});
|
||||||
|
|
||||||
// Check if the screenshot temporary file was correctly removed
|
// Check if the screenshot temporary file was correctly removed
|
||||||
body.params.options.should.not.have.a.property('screenshot');
|
body.params.options.should.not.have.a.property('screenshot');
|
||||||
@@ -188,7 +191,8 @@ describe('api', function() {
|
|||||||
url: serverUrl + '/api/runs',
|
url: serverUrl + '/api/runs',
|
||||||
body: {
|
body: {
|
||||||
url: wwwUrl + '/simple-page.html',
|
url: wwwUrl + '/simple-page.html',
|
||||||
waitForResponse: false
|
waitForResponse: false,
|
||||||
|
jsTimeline: true
|
||||||
},
|
},
|
||||||
json: true,
|
json: true,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ describe('screenshotHandler', function() {
|
|||||||
|
|
||||||
it('should respond a temporary file', function() {
|
it('should respond a temporary file', function() {
|
||||||
var file = screenshot.getTmpFilePath();
|
var file = screenshot.getTmpFilePath();
|
||||||
file.should.have.string('/screenshot.jpg');
|
file.should.have.string('/screenshot.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ describe('screenshotHandler', function() {
|
|||||||
it('should delete the temp folder with the screenshot inside', function(done) {
|
it('should delete the temp folder with the screenshot inside', function(done) {
|
||||||
screenshot = ScreenshotHandler.getScreenshotTempFile();
|
screenshot = ScreenshotHandler.getScreenshotTempFile();
|
||||||
var tmpFolderPath = screenshot.getTmpFolder().path;
|
var tmpFolderPath = screenshot.getTmpFolder().path;
|
||||||
var tmpImagePath = path.join(tmpFolderPath, 'screenshot.jpg');
|
var tmpImagePath = path.join(tmpFolderPath, 'screenshot.png');
|
||||||
|
|
||||||
// Copy image
|
// Copy image
|
||||||
var testImage = fs.readFileSync(imagePath);
|
var testImage = fs.readFileSync(imagePath);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ describe('index.js', function() {
|
|||||||
data.toolsResults.phantomas.should.have.a.property('offenders').that.is.an('object');
|
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.should.have.a.property('DOMelementMaxDepth');
|
||||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth.should.have.length(1);
|
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
|
// Test rules
|
||||||
data.should.have.a.property('rules').that.is.an('object');
|
data.should.have.a.property('rules').that.is.an('object');
|
||||||
@@ -79,7 +79,7 @@ describe('index.js', function() {
|
|||||||
"count": 1,
|
"count": 1,
|
||||||
"tree": {
|
"tree": {
|
||||||
"body": {
|
"body": {
|
||||||
"h1[1]": 1
|
"h1[0]": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ describe('phantomasWrapper', function() {
|
|||||||
options: {}
|
options: {}
|
||||||
}
|
}
|
||||||
}).then(function(data) {
|
}).then(function(data) {
|
||||||
|
/*jshint -W030 */
|
||||||
|
|
||||||
data.should.be.an('object');
|
data.should.be.an('object');
|
||||||
data.should.have.a.property('generator');
|
data.should.have.a.property('generator');
|
||||||
data.generator.should.contain('phantomas');
|
data.generator.should.contain('phantomas');
|
||||||
data.should.have.a.property('url').that.equals(url);
|
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('metrics').that.is.an('object').not.empty;
|
||||||
data.should.have.a.property('offenders').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.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
@@ -68,14 +69,15 @@ describe('phantomasWrapper', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).then(function(data) {
|
}).then(function(data) {
|
||||||
|
/*jshint -W030 */
|
||||||
|
|
||||||
data.should.be.an('object');
|
data.should.be.an('object');
|
||||||
data.should.have.a.property('generator');
|
data.should.have.a.property('generator');
|
||||||
data.generator.should.contain('phantomas');
|
data.generator.should.contain('phantomas');
|
||||||
data.should.have.a.property('url').that.equals(url);
|
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('metrics').that.is.an('object').not.empty;
|
||||||
data.should.have.a.property('offenders').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.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Simple page</title>
|
||||||
|
<script src="jquery1.8.3.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Simple page</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>li 1</li>
|
||||||
|
<li>li 2</li>
|
||||||
|
<li>li 3</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul id="foo">
|
||||||
|
<li>li4</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
// Let's start the spaghetti code!!!
|
||||||
|
|
||||||
|
$('ul').find('li');
|
||||||
|
$('li', $('#foo'));
|
||||||
|
var foo = document.getElementById('foo');
|
||||||
|
$('li', foo);
|
||||||
|
foo.getElementsByClassName('bar');
|
||||||
|
document.getElementsByClassName('bar');
|
||||||
|
foo.querySelector('ul li');
|
||||||
|
document.querySelector('ul li');
|
||||||
|
foo.querySelectorAll('ul li');
|
||||||
|
document.querySelectorAll('ul li');
|
||||||
|
|
||||||
|
var li = document.createElement('li');
|
||||||
|
foo.insertBefore(li, foo.children[0]);
|
||||||
|
|
||||||
|
li.addEventListener('click', function() {});
|
||||||
|
|
||||||
|
var $li = $(li);
|
||||||
|
$li.html();
|
||||||
|
$li.html('some content');
|
||||||
|
$('li').html();
|
||||||
|
$li.html('');
|
||||||
|
$('select').html();
|
||||||
|
|
||||||
|
$li.append('<div>woot</div>');
|
||||||
|
$li.append(document.createElement('span'));
|
||||||
|
$li.append($('<b>what</b>'));
|
||||||
|
$li.append(['<div>test1</div>', document.createElement('div')]);
|
||||||
|
$li.append('<div>test3</div>', '<div>test4</div>');
|
||||||
|
|
||||||
|
$('<div>test4</div>').appendTo($li);
|
||||||
|
$('<div>test5</div>').appendTo('<div class="bar"></div>');
|
||||||
|
|
||||||
|
$('li').prepend('<span class="prepended">prepended!</span>');
|
||||||
|
$('<div>test0</div>').prependTo($li);
|
||||||
|
|
||||||
|
$('<div>before bar</div>').before('.bar');
|
||||||
|
$('<div>before $li</div>').insertBefore($li);
|
||||||
|
|
||||||
|
$li.after('textnode after');
|
||||||
|
$('<div>after $li</div>').insertAfter($li);
|
||||||
|
|
||||||
|
$('.prepended').remove('.someClass');
|
||||||
|
$('.prepended').remove();
|
||||||
|
$('.prepended').detach();
|
||||||
|
|
||||||
|
$li.find('div').empty();
|
||||||
|
var $ulClones = $('ul').clone();
|
||||||
|
|
||||||
|
$('span').replaceWith('<span class="newspan">New span</span>');
|
||||||
|
$('<span class="newnewspan">New new span</span>').replaceAll('.newspan');
|
||||||
|
|
||||||
|
$('li').text();
|
||||||
|
$('li').eq(0).text('New li 1');
|
||||||
|
|
||||||
|
$('li').eq(0).wrap('<div></div>');
|
||||||
|
$('ul').wrapAll('<div class="ulWrapper"></div>');
|
||||||
|
$('.ulWrapper').wrapInner('<div class="doubleUlWrapper"></div>');
|
||||||
|
$('.doubleUlWrapper').unwrap();
|
||||||
|
|
||||||
|
$li.css('color', '#F00');
|
||||||
|
$li.css('color');
|
||||||
|
$li.css({'font-size': '1.2em', 'background': '#BFF'});
|
||||||
|
$li.offset();
|
||||||
|
$li.offset({left: 12, top: 23});
|
||||||
|
$li.offset(function() {return {left: 55, top: 44};});
|
||||||
|
$li.height();
|
||||||
|
|
||||||
|
$li.hide();
|
||||||
|
$li.hide(120);
|
||||||
|
$li.hide({duration: 133, done: function() {console.log(12);}});
|
||||||
|
$li.hide(222, 'swing', function() {console.log(9);});
|
||||||
|
$li.toggle();
|
||||||
|
$li.toggle(true);
|
||||||
|
$li.toggle(false);
|
||||||
|
$li.toggle(200);
|
||||||
|
|
||||||
|
var handler = function() {};
|
||||||
|
var $tbody = $('#dataTable tbody');
|
||||||
|
var $tbodytr = $('#dataTable tbody tr');
|
||||||
|
$tbodytr.on('click', handler);
|
||||||
|
$tbody.on('click', 'tr', handler);
|
||||||
|
$tbody.on('click', {someData: 'Joe'}, handler);
|
||||||
|
$tbodytr.off('click', handler);
|
||||||
|
$tbodytr.off('click', 'tr');
|
||||||
|
$tbodytr.off('click');
|
||||||
|
$tbodytr.off();
|
||||||
|
$tbody.delegate('tr', 'click', handler);
|
||||||
|
$tbody.undelegate('tr', 'click');
|
||||||
|
$tbody.undelegate('someNamespage');
|
||||||
|
$tbody.undelegate();
|
||||||
|
$tbody.one('zoop', handler);
|
||||||
|
$tbody.bind('zoop', handler);
|
||||||
|
$tbody.unbind('zoop', handler);
|
||||||
|
$tbody.bind();
|
||||||
|
|
||||||
|
$tbody.blur(handler);
|
||||||
|
$tbody.change(handler);
|
||||||
|
$tbody.click(handler);
|
||||||
|
$tbody.dblclick(handler);
|
||||||
|
$tbody.error(handler);
|
||||||
|
$tbody.focus(handler);
|
||||||
|
$tbody.focusin(handler);
|
||||||
|
$tbody.focusout(handler);
|
||||||
|
$tbody.hover(handler);
|
||||||
|
$tbody.keydown(handler);
|
||||||
|
$tbody.keypress(handler);
|
||||||
|
$tbody.keyup(handler);
|
||||||
|
$tbody.load(handler);
|
||||||
|
$tbody.mousedown(handler);
|
||||||
|
$tbody.mouseenter(handler);
|
||||||
|
$tbody.mouseleave(handler);
|
||||||
|
$tbody.mousemove(handler);
|
||||||
|
$tbody.mouseout(handler);
|
||||||
|
$tbody.mouseover(handler);
|
||||||
|
$tbody.mouseup(handler);
|
||||||
|
$tbody.resize(handler);
|
||||||
|
$tbody.scroll(handler);
|
||||||
|
$tbody.select(handler);
|
||||||
|
$tbody.submit(handler);
|
||||||
|
$tbody.unload(handler);
|
||||||
|
|
||||||
|
$tbody.attr({alt: "Beijing Brush Seller", title: "photo by Kelly Clark"});
|
||||||
|
$tbody.attr('alt');
|
||||||
|
$tbody.attr('alt', 'An alternative alt');
|
||||||
|
$tbody.attr('alt', '');
|
||||||
|
$tbody.removeAttr('alt');
|
||||||
|
|
||||||
|
$tbody.prop({checked: true});
|
||||||
|
$tbody.prop('checked');
|
||||||
|
$tbody.prop('checked', true);
|
||||||
|
$tbody.prop('checked', '');
|
||||||
|
$tbody.removeProp('checked');
|
||||||
|
|
||||||
|
var $input = $('<input type="text"></input>');
|
||||||
|
$input.val('some value');
|
||||||
|
$input.val();
|
||||||
|
|
||||||
|
$tbody.addClass(function() {return 'foo';});
|
||||||
|
$tbody.hasClass('foo');
|
||||||
|
$tbody.removeClass('foo');
|
||||||
|
$tbody.toggleClass('foo');
|
||||||
|
$tbody.toggleClass('foo', true);
|
||||||
|
$tbody.toggleClass();
|
||||||
|
|
||||||
|
$li.children();
|
||||||
|
$li.children('span');
|
||||||
|
$li.closest('ul');
|
||||||
|
$li.closest('ul', document.getElementById('foo'));
|
||||||
|
$li.find('span');
|
||||||
|
$li.find(document.getElementById('foo'));
|
||||||
|
$li.find($li);
|
||||||
|
$li.next();
|
||||||
|
$li.next('li');
|
||||||
|
$li.nextAll();
|
||||||
|
$li.nextAll('li');
|
||||||
|
$li.nextUntil('.theEnd', 'li');
|
||||||
|
$li.offsetParent();
|
||||||
|
$li.prevAll();
|
||||||
|
$li.prevAll('li');
|
||||||
|
$li.prevUntil('.theBeginning', 'li');
|
||||||
|
$li.parent();
|
||||||
|
$li.parent('ul');
|
||||||
|
$li.parents();
|
||||||
|
$li.parents('div');
|
||||||
|
$li.parentsUntil('ul');
|
||||||
|
$li.parentsUntil('body', 'div');
|
||||||
|
$li.siblings();
|
||||||
|
$li.siblings('li');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user