Browse Source

Back to a clean phantomas version + modules

Gaël Métais 11 years ago
parent
commit
df4c93356e

+ 5 - 1
app/public/scripts/resultsController.js

@@ -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
app/views/results.html

@@ -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
package.json

@@ -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 - 0
phantomas_custom/core/scopeYLT/scopeYLT.js

@@ -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'));
+    });
+};

+ 154 - 0
phantomas_custom/modules/domComplexYLT/domComplexYLT.js

@@ -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 - 0
phantomas_custom/modules/domQYLT/domQYLT.js

@@ -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 - 0
phantomas_custom/modules/jQYLT/jQYLT.js

@@ -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);
+        });
+    });
+};

+ 42 - 0
phantomas_custom/modules/jsErrYLT/jsErrYLT.js

@@ -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(' / ')
+        });*/
+    });
+};

+ 31 - 0
phantomas_custom/modules/jsTreeYLT/jsTreeYLT.js

@@ -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
server.js

@@ -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');