Back to a clean phantomas version + modules

This commit is contained in:
Gaël Métais
2014-08-05 00:08:22 +02:00
parent aba5ef5a51
commit df4c93356e
10 changed files with 988 additions and 10 deletions
+5 -1
View File
@@ -7,11 +7,15 @@ app.controller('ResultsCtrl', function ($scope) {
$scope.slowRequestsOn = false;
$scope.slowRequestsLimit = 5;
if ($scope.phantomasResults.offenders && $scope.phantomasResults.offenders.javascriptExecutionTree) {
$scope.javascript = JSON.parse($scope.phantomasResults.offenders.javascriptExecutionTree);
}
$scope.onNodeDetailsClick = function(node) {
var isOpen = node.data.showDetails;
if (!isOpen) {
// Close all other nodes
$scope.phantomasResults.javascript.children.forEach(function(currentNode) {
$scope.javascript.children.forEach(function(currentNode) {
currentNode.data.showDetails = false;
});
+20 -6
View File
@@ -18,13 +18,13 @@
<div class="ng-cloak">
<div>Tested url: <a class="testedUrl" href="phantomasResults.url" target="_blank">{{phantomasResults.url}}</a></div>
<div ng-if="phantomasResults.error || !phantomasResults.javascript.data">
<div ng-if="phantomasResults.error || !javascript">
<h2>Error: {{phantomasResults.error}}</h2>
<div ng-if="phantomasResults.error == 252">Phantomas timed out</div>
<div ng-if="phantomasResults.error == 253">Phantomas config error</div>
<div ng-if="phantomasResults.error == 254">Phantomas failed to load page</div>
<div ng-if="phantomasResults.error == 255">Phantomas internal error</div>
<div ng-if="!phantomasResults.error && !phantomasResults.javascript.data">Javascript execution tree error</div>
<div ng-if="!phantomasResults.error && !javascript">Javascript execution tree error</div>
</div>
<div ng-if="!phantomasResults.error" class="execution">
@@ -49,12 +49,26 @@
<div><!-- details --></div>
<div>Duration</div>
</div>
<div ng-repeat="node in phantomasResults.javascript.children"
ng-if="(!slowRequestsOn || node.data.time > slowRequestsLimit) && (!textFilterOn || !textFilter.length || node.data.type.indexOf(textFilter) >= 0 || node.data.callDetails.arguments[0].indexOf(textFilter) >= 0)"
ng-class="{showingDetails: node.data.showDetails, jsError: node.data.type == 'error'}">
<div ng-repeat="node in javascript.children"
ng-if="(!slowRequestsOn || node.data.time > slowRequestsLimit)
&& (!textFilterOn
|| !textFilter.length
|| node.data.type.indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[0].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[1].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[2].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[3].indexOf(textFilter) >= 0)"
ng-class="{showingDetails: node.data.showDetails, jsError: node.data.type == 'error' || node.data.type == 'jQuery version change'}">
<div class="index">{{$index}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">{{node.data.callDetails.arguments[0]}}</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">
<button ng-click="onNodeDetailsClick(node)">i</button>
<div class="detailsOverlay" ng-show="node.data.showDetails">
+1 -1
View File
@@ -6,7 +6,7 @@
"url": "git://github.com/gmetais/YellowLabTools.git"
},
"dependencies": {
"phantomas": "git+https://git@github.com/gmetais/phantomas.git#jquery-profiler",
"phantomas": "^1.5.0",
"express": "^4.6.1",
"async": "^0.9.0",
"socket.io": "^1.0.6",
+176
View File
@@ -0,0 +1,176 @@
/**
* Overwritting the original spying functions in scope.js
* This is done so we now have a before AND an after callback on the spy
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
'use strict';
exports.version = '0.1';
exports.module = function(phantomas) {
phantomas.once('init', function() {
phantomas.evaluate(function(deepAnalysis) {
(function(phantomas) {
// Overwritting phantomas spy function
(function() {
var enabled = true;
// turn off spying to not include internal phantomas actions
function spyEnabled(state, reason) {
enabled = (state === true);
phantomas.log('Spying ' + (enabled ? 'enabled' : 'disabled') + (reason ? ' - ' + reason : ''));
}
phantomas.log('Overwritting phantomas spy function');
function spy(obj, fn, callbackBefore, callbackAfter) {
var origFn = obj[fn];
if (typeof origFn !== 'function') {
return false;
}
phantomas.log('Attaching a spy to "' + fn + '" function...');
obj[fn] = function() {
var result;
// Before
if (enabled) {
var args = Array.prototype.slice.call(arguments);
callbackBefore.apply(this, args);
}
// Execute
try {
result = origFn.apply(this, arguments);
} catch(e) {
phantomas.log('Error catched on spyed function "' + fn + '"": ' + e);
} finally {
// After
if (enabled && callbackAfter) {
var args = Array.prototype.slice.call(arguments);
callbackAfter.apply(this, args);
}
}
return result;
};
// copy custom properties of original function to the mocked one
Object.keys(origFn).forEach(function(key) {
obj[fn][key] = origFn[key];
});
obj[fn].prototype = origFn.prototype;
return true;
}
phantomas.spyEnabled = spyEnabled;
phantomas.spy = spy;
})();
// Adding some code for the Javascript execution tree construction
(function() {
var root = new ContextTreeNode(null, {type: 'main'});
var currentContext = root;
if (deepAnalysis) {
phantomas.log('Entering deep Javascript analysis mode');
}
var depth = 0;
// Add a child but don't enter his context
function pushContext(data) {
if (depth === 0 || deepAnalysis) {
data.timestamp = Date.now();
currentContext.addChild(data);
}
}
// Add a child to the current context and enter his context
function enterContext(data) {
if (depth === 0 || deepAnalysis) {
data.timestamp = Date.now();
currentContext = currentContext.addChild(data);
}
depth ++;
}
// Save given data in the current context and jump change current context to its parent
function leaveContext() {
if (depth === 1 || deepAnalysis) {
currentContext.time = Date.now() - currentContext.data.timestamp;
var parent = currentContext.parent;
if (parent === null) {
console.error('Error: trying to close root context in ContextTree');
} else {
currentContext = parent;
}
}
depth --;
}
function getContextData() {
return currentContext.data;
}
// Returns a clean object, without the parent which causes recursive loops
function readFullTree() {
// Return null if the contextTree is not correctly closed
if (root !== currentContext) {
return null;
}
var current = currentContext;
function recusiveRead(node) {
if (node.children.length === 0) {
delete node.children;
} else {
for (var i=0, max=node.children.length ; i<max ; i++) {
recusiveRead(node.children[i]);
}
}
delete node.parent;
}
recusiveRead(root);
return root;
}
function ContextTreeNode(parent, data) {
this.data = data;
this.parent = parent;
this.children = [];
this.addChild = function(data) {
var child = new ContextTreeNode(this, data);
this.children.push(child);
return child;
}
}
phantomas.log('Adding some contextTree functions to phantomas');
phantomas.pushContext = pushContext;
phantomas.enterContext = enterContext;
phantomas.leaveContext = leaveContext;
phantomas.getContextData = getContextData;
phantomas.readFullTree = readFullTree;
})();
})(window.__phantomas);
}, phantomas.getParam('js-deep-analysis'));
});
};
@@ -0,0 +1,154 @@
/**
* Analyzes DOM complexity
*/
/* global document: true, Node: true, window: true */
'use strict';
exports.version = '0.3.a';
exports.module = function(phantomas) {
// total length of HTML comments (including <!-- --> brackets)
phantomas.setMetric('commentsSize'); // @desc the size of HTML comments on the page @offenders
// total length of HTML of hidden elements (i.e. display: none)
phantomas.setMetric('hiddenContentSize'); // @desc the size of content of hidden elements on the page (with CSS display: none) @offenders
// total length of text nodes with whitespaces only (i.e. pretty formatting of HTML)
phantomas.setMetric('whiteSpacesSize'); // @desc the size of text nodes with whitespaces only
// count all tags
phantomas.setMetric('DOMelementsCount'); // @desc total number of HTML element nodes
phantomas.setMetric('DOMelementMaxDepth'); // @desc maximum level on nesting of HTML element node
phantomas.setMetric('DOMidDuplicated'); // @desc duplicated id found in DOM
// nodes with inlines CSS (style attribute)
phantomas.setMetric('nodesWithInlineCSS'); // @desc number of nodes with inline CSS styling (with style attribute) @offenders
// HTML size
phantomas.on('report', function() {
phantomas.setMetricEvaluate('bodyHTMLSize', function() { // @desc the size of body tag content (document.body.innerHTML.length)
return document.body && document.body.innerHTML.length || 0;
});
phantomas.evaluate(function() {
(function(phantomas) {
var runner = new phantomas.nodeRunner(),
whitespacesRegExp = /^\s+$/,
DOMelementMaxDepth = 0,
size = 0;
// include all nodes
runner.isSkipped = function(node) {
return false;
};
runner.walk(document.body, function(node, depth) {
switch (node.nodeType) {
case Node.COMMENT_NODE:
size = node.textContent.length + 7; // '<!--' + '-->'.length
phantomas.incrMetric('commentsSize', size);
// log HTML comments bigger than 64 characters
if (size > 64) {
phantomas.addOffender('commentsSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
}
break;
case Node.ELEMENT_NODE:
phantomas.incrMetric('DOMelementsCount');
DOMelementMaxDepth = Math.max(DOMelementMaxDepth, depth);
if (node.id) {
// Send id to a collection so that duplicated ids can be counted
phantomas.emit('domId', node.id);
}
// ignore inline <script> tags
if (node.nodeName === 'SCRIPT') {
return false;
}
// @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle
var styles = window.getComputedStyle(node);
if (styles && styles.getPropertyValue('display') === 'none') {
if (typeof node.innerHTML === 'string') {
size = node.innerHTML.length;
phantomas.incrMetric('hiddenContentSize', size);
// log hidden containers bigger than 1 kB
if (size > 1024) {
phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
}
}
// don't run for child nodes as they're hidden as well
return false;
}
// count nodes with inline CSS
if (node.hasAttribute('style')) {
phantomas.incrMetric('nodesWithInlineCSS');
phantomas.addOffender('nodesWithInlineCSS', phantomas.getDOMPath(node) + ' (' + node.getAttribute('style') + ')');
}
break;
case Node.TEXT_NODE:
if (whitespacesRegExp.test(node.textContent)) {
phantomas.incrMetric('whiteSpacesSize', node.textContent.length);
}
break;
}
});
phantomas.setMetric('DOMelementMaxDepth', DOMelementMaxDepth);
phantomas.spyEnabled(false, 'counting iframes and images');
// count <iframe> tags
phantomas.setMetric('iframesCount', document.querySelectorAll('iframe').length); // @desc number of iframe nodes
// <img> nodes without dimensions (one of width / height missing)
phantomas.setMetric('imagesWithoutDimensions', (function() { // @desc number of <img> nodes without both width and height attribute @offenders
var imgNodes = document.body && document.body.querySelectorAll('img') || [],
node,
imagesWithoutDimensions = 0;
for (var i=0, len=imgNodes.length; i<len; i++) {
node = imgNodes[i];
if (!node.hasAttribute('width') || !node.hasAttribute('height')) {
phantomas.addOffender('imagesWithoutDimensions', phantomas.getDOMPath(node));
imagesWithoutDimensions++;
}
}
return imagesWithoutDimensions;
})());
phantomas.spyEnabled(true);
}(window.__phantomas));
});
});
// count ids in DOM to detect duplicated ids
// @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
var Collection = require('../../../node_modules/phantomas/lib/collection'),
DOMids = new Collection();
phantomas.on('domId', function(id) {
DOMids.push(id);
});
phantomas.on('report', function() {
phantomas.log('Looking for duplicated DOM ids');
DOMids.sort().forEach(function(id, cnt) {
if (cnt > 1) {
phantomas.incrMetric('DOMidDuplicated');
phantomas.addOffender('DOMidDuplicated', '%s: %d', id, cnt);
}
});
});
};
+214
View File
@@ -0,0 +1,214 @@
/**
* Analyzes DOM queries done via native DOM methods
*/
/* global Element: true, Document: true, Node: true, window: true */
'use strict';
exports.version = '0.7.a';
exports.module = function(phantomas) {
phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls
phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts
phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of duplicated DOM queries
// fake native DOM functions
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function querySpy(type, query, fnName) {
phantomas.emit('domQuery', type, query, fnName); // @desc DOM query has been made
}
phantomas.spy(Document.prototype, 'getElementById', function(id) {
phantomas.incrMetric('DOMqueriesById');
querySpy('id', '#' + id, 'getElementById');
phantomas.enterContext({
type: 'getElementById',
callDetails: {
context: {
domElement: '#document'
},
arguments: ['#' + id]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
}, phantomas.leaveContext);
// selectors by class name
function selectorClassNameSpyBefore(className) {
phantomas.incrMetric('DOMqueriesByClassName');
phantomas.addOffender('DOMqueriesByClassName', '.' + className);
querySpy('class', '.' + className, 'getElementsByClassName');
phantomas.enterContext({
type: 'getElementsByClassName',
callDetails: {
context: {
domElement: phantomas.getDOMPath(this)
},
arguments: ['.' + className]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
}
phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, phantomas.leaveContext);
phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, phantomas.leaveContext);
// selectors by tag name
function selectorTagNameSpyBefore(tagName) {
phantomas.incrMetric('DOMqueriesByTagName');
phantomas.addOffender('DOMqueriesByTagName', tagName);
querySpy('tag name', tagName, 'getElementsByTagName');
phantomas.enterContext({
type: 'getElementsByTagName',
callDetails: {
context: {
domElement: phantomas.getDOMPath(this)
},
arguments: [tagName]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
}
phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, phantomas.leaveContext);
phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, phantomas.leaveContext);
// selector queries
function selectorQuerySpy(selector) {
phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
phantomas.addOffender('DOMqueriesByQuerySelectorAll', selector);
querySpy('selector', selector, 'querySelectorAll');
}
function selectorQuerySpyBefore(selector) {
selectorQuerySpy(selector);
phantomas.enterContext({
type: 'querySelector',
callDetails: {
context: {
domElement: phantomas.getDOMPath(this)
},
arguments: [selector]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
}
function selectorAllQuerySpyBefore(selector) {
selectorQuerySpy(selector);
phantomas.enterContext({
type: 'querySelectorAll',
callDetails: {
context: {
domElement: phantomas.getDOMPath(this)
},
arguments: [selector]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
}
phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, phantomas.leaveContext);
phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, phantomas.leaveContext);
phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, phantomas.leaveContext);
phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, phantomas.leaveContext);
// count DOM inserts
function appendChild(child) {
/* jshint validthis: true */
// ignore appending to the node that's not yet added to DOM tree
if (!this.parentNode) {
return;
}
var destNodePath = phantomas.getDOMPath(this),
appendedNodePath = phantomas.getDOMPath(child);
// don't count elements added to fragments as a DOM inserts (issue #350)
// DocumentFragment > div[0]
if (destNodePath.indexOf('DocumentFragment') === 0) {
return;
}
phantomas.incrMetric('DOMinserts');
phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appendedNodePath, destNodePath);
phantomas.log('DOM insert: node "%s" appended to "%s"', appendedNodePath, destNodePath);
}
function appendChildSpyBefore(child) {
appendChild(child);
phantomas.enterContext({
type: 'appendChild',
callDetails: {
context: {
domElement: phantomas.getDOMPath(this)
},
arguments: [phantomas.getDOMPath(child)]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
}
function insertBeforeSpyBefore(child) {
appendChild(child);
phantomas.enterContext({
type: 'insertBefore',
callDetails: {
context: {
domElement: phantomas.getDOMPath(this)
},
arguments: [phantomas.getDOMPath(child)]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
}
phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, phantomas.leaveContext);
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, phantomas.leaveContext);
})(window.__phantomas);
});
});
// count DOM queries by either ID, tag name, class name and selector query
// @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
var Collection = require('../../../node_modules/phantomas/lib/collection'),
DOMqueries = new Collection();
phantomas.on('domQuery', function(type, query, fnName) {
phantomas.log('DOM query: by %s - "%s" (using %s)', type, query, fnName);
phantomas.incrMetric('DOMqueries');
DOMqueries.push(type + ' "' + query + '"');
});
phantomas.on('report', function() {
DOMqueries.sort().forEach(function(query, cnt) {
if (cnt > 1) {
phantomas.incrMetric('DOMqueriesDuplicated');
phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt);
}
});
});
};
+312
View File
@@ -0,0 +1,312 @@
/**
* Analyzes jQuery activity
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
'use strict';
exports.version = '0.2.a';
exports.module = function(phantomas) {
phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
phantomas.setMetric('jQuerySizzleCallsDuplicated'); // @desc number of calls on the same Sizzle request
phantomas.setMetric('jQueryBindOnMultipleElements'); //@desc number of calls to jQuery bind function on 2 or more elements
phantomas.setMetric('jQueryDifferentVersions'); //@desc number of different jQuery versions loaded on the page (not counting iframes)
var jQueryFunctions = [
// DOM manipulations
'html',
'append',
'appendTo',
'prepend',
'prependTo',
'before',
'insertBefore',
'after',
'insertAfter',
'remove',
'detach',
'empty',
'clone',
'replaceWith',
'replaceAll',
'text',
'wrap',
'wrapAll',
'wrapInner',
'unwrap',
// Style manipulations
'css',
'offset',
'position',
'height',
'innerHeight',
'outerHeight',
'width',
'innerWidth',
'outerWidth',
'scrollLeft',
'scrollTop',
// generic events
'on',
'off',
'live',
'die',
'delegate',
'undelegate',
'one',
'trigger',
'triggerHandler',
'unbind',
// more events
'blur',
'change',
'click',
'dblclick',
'error',
'focus',
'focusin',
'focusout',
'hover',
'keydown',
'keypress',
'keyup',
'load',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
'resize',
'scroll',
'select',
'submit',
'toggle',
'unload',
// attributes
'attr',
'prop',
'removeAttr',
'removeProp',
'val',
'hasClass',
'addClass',
'removeClass',
'toggleClass'
];
// spy calls to jQuery functions
phantomas.once('init', function() {
phantomas.evaluate(function(jQueryFunctions) {
(function(phantomas) {
var jQuery;
// TODO: create a helper - phantomas.spyGlobalVar() ?
window.__defineSetter__('jQuery', function(val) {
var version;
var oldJQuery = jQuery;
if (!val || !val.fn) {
phantomas.log('jQuery: unable to detect version!');
return;
}
version = val.fn.jquery;
jQuery = val;
jQueryFn = val.fn;
// Older jQuery (v?.?) compatibility
if (!jQueryFn) {
jQueryFn = jQuery;
}
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
phantomas.emit('jQueryLoaded', version);
phantomas.pushContext({
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
callDetails: {
arguments: ['version ' + version]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
// jQuery.ready.promise
// works for jQuery 1.8.0+ (released Aug 09 2012)
phantomas.spy(val.ready, 'promise', function(func) {
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
phantomas.pushContext({
type: 'jQuery - onDOMReady',
callDetails: {
arguments: [func]
},
caller: phantomas.getCaller(1),
backtrace: phantomas.getBacktrace()
});
}) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!');
// Sizzle calls - jQuery.find
// works for jQuery 1.3+ (released Jan 13 2009)
phantomas.spy(val, 'find', function(selector, context) {
phantomas.incrMetric('jQuerySizzleCalls');
phantomas.emit('onSizzleCall', selector + ' (context: ' + (phantomas.getDOMPath(context) || 'unknown') + ')');
phantomas.enterContext({
type: 'jQuery - find',
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(context)
},
arguments: [selector]
},
caller: phantomas.getCaller(3),
backtrace: phantomas.getBacktrace()
});
}, phantomas.leaveContext) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
// $().bind - jQuery.bind
// works for jQuery v?.?
phantomas.spy(jQueryFn, 'bind', function(eventTypes, func) {
phantomas.enterContext({
type: 'jQuery - bind',
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(this[0]),
selector: this.selector
},
arguments: [eventTypes, func]
},
caller: phantomas.getCaller(3),
backtrace: phantomas.getBacktrace()
});
}, function(eventTypes, func) {
phantomas.leaveContext();
if (this.length > 1) {
phantomas.incrMetric('jQueryBindOnMultipleElements');
phantomas.addOffender('jQueryBindOnMultipleElements', '%s (%s on %d elements)', this.selector, eventTypes, this.length);
}
}) || phantomas.log('jQuery: can not measure jQueryBindCalls (jQuery used on the page is too old)!');
// Add spys on many jQuery functions
jQueryFunctions.forEach(function(functionName) {
var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
phantomas.spy(jQueryFn, functionName, function(args) {
// Clean args
args = [].slice.call(arguments);
args.forEach(function(arg, index) {
if (arg instanceof jQuery) {
arg = phantomas.getDOMPath(arg[0]) || 'unknown';
}
if (arg instanceof Object) {
try {
arg = JSON.stringify(arg);
} catch(e) {
arg = '[Object]';
}
}
if ((typeof arg == 'string' || arg instanceof String) && arg.length > 200) {
arg = arg.substring(0, 200) + '...';
}
if (arg === true) {
arg = 'true';
}
if (arg === false) {
arg = 'false';
}
if (arg == null) {
arg = 'null';
}
if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) {
arg = 'undefined';
}
args[index] = arg;
});
phantomas.enterContext({
type: 'jQuery - ' + functionName,
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(this[0])
},
arguments: args
},
caller: phantomas.getCaller(3),
backtrace: phantomas.getBacktrace()
});
}, phantomas.leaveContext) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)');
});
});
window.__defineGetter__('jQuery', function() {
return jQuery;
});
})(window.__phantomas);
}, jQueryFunctions);
});
// count Sizzle calls to detect duplicated queries
var Collection = require('../../../node_modules/phantomas/lib/collection'),
sizzleCalls = new Collection(),
jQueryLoading = new Collection();
phantomas.on('onSizzleCall', function(request) {
sizzleCalls.push(request);
});
phantomas.on('jQueryLoaded', function(version) {
jQueryLoading.push(version);
});
phantomas.on('report', function() {
sizzleCalls.sort().forEach(function(id, cnt) {
if (cnt > 1) {
phantomas.incrMetric('jQuerySizzleCallsDuplicated');
phantomas.addOffender('jQuerySizzleCallsDuplicated', '%s: %d', id, cnt);
}
});
jQueryLoading.forEach(function(version) {
phantomas.incrMetric('jQueryDifferentVersions');
phantomas.addOffender('jQueryDifferentVersions', '%s', version);
});
});
};
@@ -0,0 +1,42 @@
/**
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
*/
'use strict';
exports.version = '0.3.a';
exports.module = function(phantomas) {
phantomas.setMetric('jsErrors'); // @desc number of JavaScript errors
function formatTrace(trace) {
var ret = [];
if(Array.isArray(trace)) {
trace.forEach(function(entry) {
ret.push((entry.function ? entry.function + '(): ' : 'unknown fn: ') + (entry.sourceURL || entry.file) + ' @ ' + entry.line);
});
}
return ret;
}
phantomas.on('jserror', function(msg, trace) {
trace = formatTrace(trace);
phantomas.log(msg);
phantomas.log('Backtrace: ' + trace.join(' / '));
phantomas.incrMetric('jsErrors');
phantomas.addOffender('jsErrors', msg + ' - ' + trace.join(' / '));
// TODO : send the error back to the browser ?
/*phantomas.pushContext({
type: 'error',
callDetails: {
arguments: [msg]
},
caller: trace[0],
backtrace: trace.join(' / ')
});*/
});
};
@@ -0,0 +1,31 @@
/**
* Saves the javascript interractions with the DOM
*
* Run phantomas with --js-execution-tree option to use this module
*/
'use strict';
exports.version = '0.1';
exports.module = function(phantomas) {
phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries
// save data
phantomas.on('report', function() {
phantomas.log('Reading execution tree JSON');
phantomas.evaluate(function() {(function(phantomas) {
var fullTree = phantomas.readFullTree();
if (fullTree === null) {
phantomas.log('JS execution tree: error, the execution tree is not correctly closed');
return;
}
phantomas.setMetric('javascriptExecutionTree', true, true);
phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree));
})(window.__phantomas);});
});
};
+33 -2
View File
@@ -65,8 +65,39 @@ app.post('/launchTest', function(req, res) {
var options = {
timeout: 60,
'js-execution-tree': true,
reporter: 'json:pretty'
'js-deep-analysis': false,
reporter: 'json:pretty',
'skip-modules': [
'ajaxRequests',
'alerts',
'cacheHits',
'caching',
'console',
'cookies',
'documentHeight',
'domains',
'domComplexity',
'domMutations',
'domQueries',
'filmStrip',
'jQuery',
'jserrors',
'har',
'headers',
'localStorage',
'mainRequest',
'pageSource',
'redirects',
'requestsStats',
'screenshot',
'staticAssets',
'timeToFirst',
'waitForSelector'
].join(','),
'include-dirs': [
'phantomas_custom/core',
'phantomas_custom/modules'
].join(',')
};
console.log('Adding test ' + testId + ' on ' + url + ' to the queue');