Back to a clean phantomas version + modules
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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);});
|
||||
});
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user