瀏覽代碼

Merge pull request #58 from gmetais/profilerParams

Profiler params
Gaël Métais 10 年之前
父節點
當前提交
c47e6d0219

+ 3 - 1
Gruntfile.js

@@ -66,7 +66,9 @@ module.exports = function(grunt) {
                 'app/nodeControllers/*.js',
                 'app/public/scripts/*.js',
                 'phantomas_custom/**/*.js',
-                'test/**/*.js',
+                'test/api/*.js',
+                'test/core/*.js',
+                'test/fixtures/*.js',
                 'front/src/js/**/*.js'
             ]
         },

+ 3 - 2
front/src/css/rule.css

@@ -109,6 +109,7 @@
   border-bottom-left-radius: 0.4em;
   border-bottom-right-radius: 0.4em;
   border-top: 1px solid #999;
+  z-index: 2;
 }
 .offenders .offenderButton .domTree {
   text-align: left;
@@ -125,13 +126,13 @@
   white-space: nowrap;
   padding: 0.5em;
 }
-.offenders .offenderButton.opens.mouseOver {
+.offenders .offenderButton.opens:hover {
   border-bottom-left-radius: 0;
   border-bottom-right-radius: 0;
   background: #ffe0cc;
   z-index: 2;
 }
-.offenders .offenderButton.opens.mouseOver > div {
+.offenders .offenderButton.opens:hover > div {
   display: block;
 }
 .offendersHtml {

+ 8 - 15
front/src/css/timeline.css

@@ -143,20 +143,6 @@
   border: 1px dotted #aaa;
   text-align: left;
 }
-.slowRequestsLimit {
-  width: 3em;
-  font-size: 1em;
-  text-align: right;
-  border: 1px solid #aaa;
-}
-input.textFilter {
-  box-shadow: none;
-  font-size: 1em;
-  padding: 0 0.2em;
-  border: 1px solid #aaa;
-  border-radius: none;
-  width: 15em;
-}
 .table {
   display: table;
   width: 100%;
@@ -226,6 +212,9 @@ input.textFilter {
 .table .child span:hover {
   background: #EBD8E2;
 }
+.table .child span:hover div {
+  display: inline-block;
+}
 .table .child span:hover .childArgs {
   display: block;
   position: absolute;
@@ -259,6 +248,7 @@ input.textFilter {
   width: 0.8em;
 }
 .detailsOverlay {
+  display: none;
   position: absolute;
   right: 3em;
   top: -3em;
@@ -268,13 +258,16 @@ input.textFilter {
   background: #fff;
   border: 2px solid #f1c40f;
   border-radius: 0.5em;
-  z-index: 1;
+  z-index: 2;
 }
 @media screen and (max-width: 1024px) {
   .detailsOverlay {
     width: 25em;
   }
 }
+.showDetails .detailsOverlay {
+  display: block;
+}
 .detailsOverlay .closeBtn {
   position: absolute;
   top: 0.5em;

+ 0 - 1
front/src/js/app.js

@@ -13,7 +13,6 @@ var yltApp = angular.module('YellowLabTools', [
     'apiService',
     'menuService',
     'gradeDirective',
-    'jsChildrenDirective',
     'offendersDirectives'
 ]);
 

+ 1 - 52
front/src/js/controllers/timelineCtrl.js

@@ -52,7 +52,7 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
         var lastEvent = originalExecutions[originalExecutions.length - 1];
         $scope.endTime =  lastEvent.data.timestamp + (lastEvent.data.time || 0);
 
-        // Filter and calculate the search index
+        // Filter
         $scope.executionTree = [];
         originalExecutions.forEach(function(node) {
             
@@ -66,9 +66,6 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
                 }
             }
 
-            // Prepare a faster angular search by creating a kind of search index
-            node.searchIndex = (node.data.callDetails) ? [node.data.type].concat(node.data.callDetails.arguments).join('°°') : node.data.type;
-
             $scope.executionTree.push(node);
         });
     }
@@ -114,38 +111,6 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
         $scope.profilerData = $scope.executionTree;
     }
 
-
-    function parseBacktrace(str) {
-        if (!str) {
-            return null;
-        }
-
-        var out = [];
-        var splited = str.split(' / ');
-        splited.forEach(function(trace) {
-            var fnName = null, fileAndLine;
-
-            var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
-            if (withFnResult === null) {
-                fileAndLine = trace;
-            } else {
-                fnName = withFnResult[1];
-                fileAndLine = withFnResult[2];
-            }
-
-            var fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
-            var filePath = fileAndLineSplit[1];
-            var line = fileAndLineSplit[2];
-
-            out.push({
-                fnName: fnName,
-                filePath: filePath,
-                line: line
-            });
-        });
-        return out;
-    }
-
     $scope.changeScript = function() {
         initExecutionTree();
         initTimeline();
@@ -170,22 +135,6 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
         return lineIndex;
     };
 
-    $scope.onNodeDetailsClick = function(node) {
-        var isOpen = node.showDetails;
-        if (!isOpen) {
-            // Close all other nodes
-            $scope.executionTree.forEach(function(currentNode) {
-                currentNode.showDetails = false;
-            });
-
-            // Parse the backtrace
-            if (!node.parsedBacktrace) {
-                node.parsedBacktrace = parseBacktrace(node.data.backtrace);
-            }
-
-        }
-        node.showDetails = !isOpen;
-    };
 
     $scope.backToDashboard = function() {
         $location.path('/result/' + $scope.runId);

+ 0 - 44
front/src/js/directives/jsChildrenDirective.js

@@ -1,44 +0,0 @@
-var jsChildrenDirective = angular.module('jsChildrenDirective', []);
-
-jsChildrenDirective.directive('jsChildren', function() {
- 
-    return {
-        restrict: 'E',
-        scope: {
-            node: '=node'
-        },
-        template:   '<div class="children"></div>',
-        replace: true,
-        link: function(scope, element, attrs) {
-            
-            function recursiveHtmlBuilder(node) {
-                var html = '';
-                
-                if (node.children) {
-                    node.children.forEach(function(child) {
-                        
-                        var childArgs = '';
-                        if (child.data.callDetails && child.data.callDetails.arguments && child.data.callDetails.arguments.length > 0) {
-                            childArgs = child.data.callDetails.arguments.join(' : ');
-                            if (childArgs.length > 100) {
-                                childArgs = childArgs.substr(0, 98) + '...';
-                            }
-                        }
-
-                        html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + childArgs + '</div></span>' + recursiveHtmlBuilder(child) + '</div>';
-                    });
-                }
-
-                return html;
-            }
-
-            element.append(recursiveHtmlBuilder(scope.node));
-
-            // Bind a very special behavior:
-            // We want to display something in the next table-cell, at the same hight.
-            element.find('span').on('mouseenter', function() {
-                
-            });
-        }
-    };
-});

+ 775 - 101
front/src/js/directives/offendersDirectives.js

@@ -1,50 +1,735 @@
-var offendersDirectives = angular.module('offendersDirectives', []);
-
-offendersDirectives.directive('domTree', function() {
-    return {
-        restrict: 'E',
-        scope: {
-            tree: '='
-        },
-        template: '<div class="domTree"></div>',
-        replace: true,
-        link: function(scope, element, attrs) {
-            
-            function recursiveHtmlBuilder(tree) {
-                var html = '';
-                var keys = Object.keys(tree);
-                
-                keys.forEach(function(key) {
-                    if (isNaN(tree[key])) {
-                        html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
-                    } else if (tree[key] > 1) {
-                        html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
+(function() {
+    "use strict";
+    var offendersDirectives = angular.module('offendersDirectives', []);
+
+    function getdomTreeHTML(tree) {
+        return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
+    }
+
+    function getdomTreeInnerHTML(tree) {
+        return recursiveHtmlBuilder(tree);
+    }
+
+    function recursiveHtmlBuilder(tree) {
+        var html = '';
+        var keys = Object.keys(tree);
+        
+        keys.forEach(function(key) {
+            if (isNaN(tree[key])) {
+                html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
+            } else if (tree[key] > 1) {
+                html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
+            } else {
+                html += '<div><span>' + key + '</span></div>';
+            }
+        });
+
+        return html;
+    }
+
+    offendersDirectives.directive('domTree', function() {
+        return {
+            restrict: 'E',
+            scope: {
+                tree: '='
+            },
+            template: '<div class="domTree"></div>',
+            replace: true,
+            link: function(scope, element) {
+                element.append(getdomTreeInnerHTML(scope.tree));
+            }
+        };
+    });
+
+    function getDomElementButtonHTML(obj, onASingleLine) {
+        if (obj.tree && !onASingleLine) {
+            return '<div class="offenderButton opens">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
+        } else {
+            return '<div class="offenderButton">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
+        }
+    }
+
+    function getDomElementButtonInnerHTML(obj, onASingleLine) {
+        if (obj.type === 'html' ||
+            obj.type === 'body' ||
+            obj.type === 'head' ||
+            obj.type === 'window' ||
+            obj.type === 'document' ||
+            obj.type === 'fragment') {
+                return obj.type;
+        }
+
+        if (obj.type === 'notAnElement') {
+            return 'Incorrect element';
+        }
+
+        var html = '';
+        if (obj.type === 'domElement') {
+            html = 'DOM element <b>' + obj.element + '</b>';
+        } else if (obj.type === 'fragmentElement') {
+            html = 'Fragment element <b>' + obj.element + '</b>';
+        } else if (obj.type === 'createdElement') {
+            html = 'Created element <b>' + obj.element + '</b>';
+        }
+
+        if (obj.tree && !onASingleLine) {
+            html += getdomTreeHTML(obj.tree);
+        }
+
+        return html;
+    }
+
+    offendersDirectives.directive('domElementButton', function() {
+        return {
+            restrict: 'E',
+            scope: {
+                obj: '='
+            },
+            template: '<div class="offenderButton" ng-class="{opens: obj.tree}"></div>',
+            replace: true,
+            link: function(scope, element) {                
+                element.append(getDomElementButtonInnerHTML(scope.obj));
+            }
+        };
+    });
+
+
+    function getJQueryContextButtonHTML(context, onASingleLine) {
+        if (context.length === 0) {
+            return '<span class="offenderButton">Empty jQuery object</span>';
+        }
+
+        if (context.length === 1) {
+            return getDomElementButtonHTML(context.elements[0], onASingleLine);
+        }
+
+        var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine);
+        if (context.length === 3) {
+            html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine);
+        } else if (context.length > 3) {
+            html += ' and ' + (context.length - 2) + ' more...';
+        }
+        return html + '}';
+    }
+
+    function isJQuery(node) {
+        return node.data.type.indexOf('jQuery ') === 0;
+    }
+
+    function getNonJQueryHTML(node, onASingleLine) {
+        var type = node.data.type;
+
+        if (!node.data.callDetails) {
+            return '';
+        }
+
+        var args = node.data.callDetails.arguments;
+        var ctxt = node.data.callDetails.context;
+
+
+        switch (type) {
+            case 'getElementById':
+            case 'createElement':
+                return '<b>' + args[0] + '</b>';
+
+            case 'getElementsByClassName':
+            case 'getElementsByTagName':
+            case 'querySelector':
+            case 'querySelectorAll':
+                return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
+
+            case 'appendChild':
+                return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
+
+            case 'insertBefore':
+                return 'insert' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine);
+
+            case 'addEventListener':
+                return 'bind <b>' + args[0] + '</b> to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
+
+            case 'error':
+                return args[0];
+
+            default:
+                return '';
+        }
+    }
+
+    function getJQueryHTML(node, onASingleLine) {
+        var type = node.data.type;
+        var args = node.data.callDetails.arguments;
+        var ctxt = node.data.callDetails.context;
+        
+        // escape HTML in args
+        for (var i = 0 ; i < 4 ; i ++) {
+            if (args[i]) {
+                args[i] = escapeHTML(args[i]);
+            }
+        }
+
+        if (type === 'jQuery loaded' || type === 'jQuery version change') {
+            return args[0];
+        }
+
+        switch (type) {
+            case 'jQuery - onDOMReady':
+            case 'jQuery - windowOnLoad':
+                return '(function)';
+
+            case 'jQuery - Sizzle call':
+                return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
+
+            case 'jQuery - find':
+                if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') {
+                    return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                } else {
+                    return '<b>' + args[0] + '</b>';
+                }
+                break;
+
+            case 'jQuery - html':
+                if (args[0] !== undefined) {
+                    return 'set content "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                } else {
+                    return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - append':
+                return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - appendTo':
+                return 'append' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
+
+            case 'jQuery - prepend':
+                return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - prependTo':
+                return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
+
+            case 'jQuery - before':
+                return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - insertBefore':
+                return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before <b>' + args[0] + '</b>';
+
+            case 'jQuery - after':
+                return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - insertAfter':
+                return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after <b>' + args[0] + '</b>';
+
+            case 'jQuery - remove':
+            case 'jQuery - detach':
+                if (args[0]) {
+                    return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
+                } else {
+                    return getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - empty':
+            case 'jQuery - clone':
+            case 'jQuery - unwrap':
+            case 'jQuery - show':
+            case 'jQuery - hide':
+            case 'jQuery - animate':
+            case 'jQuery - fadeIn':
+            case 'jQuery - fadeOut':
+            case 'jQuery - fadeTo':
+            case 'jQuery - fadeToggle':
+            case 'jQuery - slideDown':
+            case 'jQuery - slideUp':
+            case 'jQuery - slideToggle':
+                return getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - replaceWith':
+                return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with <b>' + args[0] + '</b>';
+
+            case 'jQuery - replaceAll':
+                return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - text':
+                if (args[0]) {
+                    return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                } else {
+                    return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - wrap':
+            case 'jQuery - wrapAll':
+                return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
+
+            case 'jQuery - wrapInner':
+                return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
+
+            case 'jQuery - css':
+            case 'jQuery - attr':
+            case 'jQuery - prop':
+                if (isStringOfObject(args[0])) {
+                    return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                } else if (args[1]) {
+                    return 'set <b>' + args[0] + '</b> : <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                } else {
+                    return 'get <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - offset':
+            case 'jQuery - height':
+            case 'jQuery - innerHeight':
+            case 'jQuery - outerHeight':
+            case 'jQuery - width':
+            case 'jQuery - innerWidth':
+            case 'jQuery - outerWidth':
+            case 'jQuery - scrollLeft':
+            case 'jQuery - scrollTop':
+            case 'jQuery - position':
+                if (args[0]) {
+                    return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                } else {
+                    return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - toggle':
+                if (args[0] === 'true') {
+                    return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
+                } else if (args[0] === 'false') {
+                    return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
+                } else {
+                    return getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - on':
+            case 'jQuery - one':
+                if (args[1]) {
+                    return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
+                } else {
+                    return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - off':
+                if (args[0]) {
+                    if (args[1]) {
+                        return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
+                    } else {
+                        return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                    }
+                } else {
+                    return 'unbind all events';
+                }
+                break;
+
+            case 'jQuery - live':
+            case 'jQuery - bind':
+                return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - die':
+            case 'jQuery - unbind':
+                if (args[0]) {
+                    return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                } else {
+                    return 'unbind all events';
+                }
+                break;
+
+            case 'jQuery - delegate':
+                return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
+
+            case 'jQuery - undelegate':
+                if (args[0]) {
+                    if (args[1]) {
+                        return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
+                    } else {
+                        return 'unbind namespace <b>' + args[0] + '</b>';
+                    }
+                } else {
+                    return 'unbind all events';
+                }
+                break;
+
+            case 'jQuery - blur':
+            case 'jQuery - change':
+            case 'jQuery - click':
+            case 'jQuery - dblclick':
+            case 'jQuery - error':
+            case 'jQuery - focus':
+            case 'jQuery - focusin':
+            case 'jQuery - focusout':
+            case 'jQuery - hover':
+            case 'jQuery - keydown':
+            case 'jQuery - keypress':
+            case 'jQuery - keyup':
+            case 'jQuery - load':
+            case 'jQuery - mousedown':
+            case 'jQuery - mouseenter':
+            case 'jQuery - mouseleave':
+            case 'jQuery - mousemove':
+            case 'jQuery - mouseout':
+            case 'jQuery - mouseover':
+            case 'jQuery - mouseup':
+            case 'jQuery - resize':
+            case 'jQuery - scroll':
+            case 'jQuery - select':
+            case 'jQuery - submit':
+            case 'jQuery - unload':
+                return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - removeAttr':
+            case 'jQuery - removeProp':
+                return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - val':
+                if (args[0]) {
+                    return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                } else {
+                    return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - hasClass':
+            case 'jQuery - addClass':
+            case 'jQuery - removeClass':
+                return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - toggleClass':
+                if (args[0]) {
+                    if (args[1]) {
+                        return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
                     } else {
-                        html += '<div><span>' + key + '</span></div>';
+                        return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
                     }
+                } else {
+                    return 'magic no-argument toggleClass';
+                }
+                break;
+
+            case 'jQuery - children':
+                if (args[0]) {
+                    return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
+                } else {
+                    return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - closest':
+                if (args[1]) {
+                    return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
+                } else {
+                    return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - next':
+            case 'jQuery - nextAll':
+                if (args[0]) {
+                    return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
+                } else {
+                    return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - nextUntil':
+                if (args[0]) {
+                    if (args[1]) {
+                        return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
+                    } else {
+                        return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
+                    }
+                } else {
+                    return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - offsetParent':
+                return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+
+            case 'jQuery - prev':
+            case 'jQuery - prevAll':
+                if (args[0]) {
+                    return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
+                } else {
+                    return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - prevUntil':
+                if (args[0]) {
+                    if (args[1]) {
+                        return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
+                    } else {
+                        return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
+                    }
+                } else {
+                    return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - parent':
+            case 'jQuery - parents':
+                if (args[0]) {
+                    return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
+                } else {
+                    return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - parentsUntil':
+                if (args[0]) {
+                    if (args[1]) {
+                        return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
+                    } else {
+                        return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
+                    }
+                } else {
+                    return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - siblings':
+                if (args[0]) {
+                    return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
+                } else {
+                    return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
+                }
+                break;
+
+            case 'jQuery - onDOMReady':
+                return '(function)';
+
+            default:
+                return '';
+        }
+    }
+
+    function escapeHTML(html) {
+        var entityMap = {
+            "&": "&amp;",
+            "<": "&lt;",
+            ">": "&gt;",
+            '"': '&quot;',
+            "'": '&#39;',
+            "/": '&#x2F;'
+        };
+
+        return String(html).replace(/[&<>"'\/]/g, function (s) {
+            return entityMap[s];
+        });
+    }
+
+    function joinArgs(args) {
+        var html = '<b>' + args[0] + '</b>';
+        if (args[1]) {
+            html += ', <b>' + args[1] + '</b>';
+            if (args[2]) {
+                html += ', <b>' + args[2] + '</b>';
+                if (args[3]) {
+                    html += ', and more...';
+                }
+            }
+        }
+        return html;
+    }
+
+   function isStringOfObject(str) {
+        return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
+    }
+
+    function isPureString(str) {
+        return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
+    }
+
+    function getTimelineParamsHTML(node, onASingleLine) {
+        if (isJQuery(node)) {
+            return getJQueryHTML(node, onASingleLine);
+        } else {
+            return getNonJQueryHTML(node, onASingleLine);
+        }
+    }
+
+    function getBacktraceHTML(backtrace) {
+        var html = '';
+        var parsedBacktrace = parseBacktrace(backtrace);
+
+        if (!parsedBacktrace || parsedBacktrace.length === 0) {
+            html += '<div><div>can\'t find any backtrace :/</div></div>';
+        } else {
+            for (var i = 0 ; i < parsedBacktrace.length ; i++) {
+                html += '<div>';
+                html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
+                html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + ':' + parsedBacktrace[i].line + '</div>';
+                html += '</div>';
+            }
+        }
+
+        return html;
+    }
+
+    function parseBacktrace(str) {
+        if (!str) {
+            return null;
+        }
+
+        var out = [];
+        var splited = str.split(' / ');
+        
+        try {
+
+            splited.forEach(function(trace) {
+                var fnName = null, fileAndLine;
+
+                var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
+                if (withFnResult === null) {
+                    fileAndLine = trace;
+                } else {
+                    fnName = withFnResult[1];
+                    fileAndLine = withFnResult[2];
+                }
+
+                var fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
+                var filePath = fileAndLineSplit[1];
+                var line = fileAndLineSplit[2];
+
+                out.push({
+                    fnName: fnName,
+                    filePath: filePath,
+                    line: line
                 });
+            });
+
+        } catch(e) {
+            return null;
+        }
 
-                return html;
+        return out;
+    }
+
+    function getTimelineDetailsHTML(node) {
+        var html = '';
+
+        if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
+            if (node.warning || node.error) {
+                html += '<div class="icon-warning"></div>';
+            } else {
+                html += '<div class="icon-question"></div>';
+            }
+
+            html += '<div class="detailsOverlay">';
+            html += '<div class="closeBtn">✖</div>';
+
+            if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
+                html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
+            } else if (node.data.type === 'jQuery - bind' && node.data.callDetails.context.length > 5) {
+                html += '<p class="advice">The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).</p>';
+            }
+
+            if (node.data.resultsNumber === 0) {
+                html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
+            } else if (node.data.resultsNumber > 0) {
+                html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
+            }
+
+            if (node.data.backtrace) {
+                html += '<h4>Backtrace</h4>';
+                html += '<div class="table">';
+                html += getBacktraceHTML(node.data.backtrace);
+                html += '</div>';
+            }
+
+            html += '</div>';
+        }
+        
+        return html;
+    }
+
+    
+    offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
+        
+        var numberWithCommas = $filter('number');
+
+        function getProfilerLineHTML(index, node) {
+            return  '<div class="index">' + (index + 1) + '</div>' +
+                    '<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
+                    '<div class="value offenders">' + getTimelineParamsHTML(node, false) + '</div>' +
+                    '<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
+                    '<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
+        }
+
+        function recursiveChildrenHTML(node) {
+            var html = '';
+            
+            if (node.children) {
+                node.children.forEach(function(child) {
+                    html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
+                });
+            }
+
+            return html;
+        }
+
+        function onDetailsClick(row) {
+            // Close if it's alreay open
+            if (row.classList.contains('showDetails')) {
+                closeDetails(row);
+                return;
+            }
+
+            // Close any other open details overlay
+            var openOnes = document.getElementsByClassName('showDetails');
+            if (openOnes.length > 0) {
+                openOnes[0].classList.remove('showDetails');
             }
 
-            element.append(recursiveHtmlBuilder(scope.tree));
+            // Make it appear
+            row.classList.add('showDetails');
+
+            // Bind the close button
+            row.querySelector('.closeBtn').addEventListener('click', function() {
+                closeDetails(row);
+            });
         }
-    };
-});
 
-offendersDirectives.directive('domElementButton', function() {
-    return {
-        restrict: 'E',
-        scope: {
-            obj: '='
-        },
-        templateUrl: 'views/domElementButton.html',
-        replace: true
-    };
-});
+        function closeDetails(row) {
+            row.classList.remove('showDetails');
 
-offendersDirectives.filter('shortenUrl', function() {
-    return function(url, maxLength) {
+            // Unbind the close button
+            row.querySelector('.closeBtn').removeEventListener('click', closeDetails);   
+        }
+
+        return {
+            restrict: 'E',
+            scope: {
+                index: '=',
+                node: '='
+            },
+            template: '<div id="line_{{index}}"></div>',
+            replace: true,
+            link: function(scope, element) {
+                
+                if (scope.node.error) {
+                    element.addClass('jsError');
+                } else if (scope.node.windowPerformance) {
+                    element.addClass('windowPerformance');
+                }
+
+                element.append(getProfilerLineHTML(scope.index, scope.node));
+
+                // Bind click on the details icon
+                var detailsIcon = element[0].querySelector('.details div');
+                if (detailsIcon) {
+                    detailsIcon.addEventListener('click', function() {
+                        onDetailsClick(this.parentNode.parentNode);
+                    });
+                }
+            }
+        };
+    }]);
+
+    function shortenUrl(url, maxLength) {
         if (!maxLength) {
             maxLength = 110;
         }
@@ -54,67 +739,56 @@ offendersDirectives.filter('shortenUrl', function() {
         var rightLength = Math.ceil((maxLength - 5) / 2.1);
 
         return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
-    };
-});
-
-offendersDirectives.directive('urlLink', function() {
-    return {
-        restrict: 'E',
-        scope: {
-            url: '=',
-            maxLength: '='
-        },
-        template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
-        replace: true
-    };
-});
-
-offendersDirectives.filter('encodeURIComponent', function() {
-    return window.encodeURIComponent;
-});
-
-offendersDirectives.directive('fileAndLine', function() {
-    return {
-        restrict: 'E',
-        scope: {
-            file: '=',
-            line: '=',
-            column: '='
-        },
-        template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file">&lt;inline CSS&gt;</span> @ {{line}}:{{column}}</span>',
-        replace: true
-    };
-});
-
-offendersDirectives.directive('fileAndLineButton', function() {
-    return {
-        restrict: 'E',
-        scope: {
-            file: '=',
-            line: '=',
-            column: '='
-        },
-        template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
-        replace: true
-    };
-});
-
-offendersDirectives.directive('offenderButton', function() {
-    return {
-        restrict: 'C',
-        link: function(scope, element, attrs) {
-
-            console.log('initializing touchstart');
-
-            element.bind('touchstart mouseenter', function(e) {
-                element.addClass('mouseOver');
-                e.preventDefault();
-            });
+    }
 
-            element.bind('touchend mouseleave click', function(e) {
-                element.removeClass('mouseOver');
-                e.preventDefault();
-            });
-        }
-    };
-});
+    offendersDirectives.filter('shortenUrl', function() {
+        return shortenUrl;
+    });
+
+    function getUrlLink(url, maxLength) {
+        return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
+    }
+
+    offendersDirectives.directive('urlLink', function() {
+        return {
+            restrict: 'E',
+            scope: {
+                url: '=',
+                maxLength: '='
+            },
+            template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
+            replace: true
+        };
+    });
+
+    offendersDirectives.filter('encodeURIComponent', function() {
+        return window.encodeURIComponent;
+    });
+
+    offendersDirectives.directive('fileAndLine', function() {
+        return {
+            restrict: 'E',
+            scope: {
+                file: '=',
+                line: '=',
+                column: '='
+            },
+            template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file">&lt;inline CSS&gt;</span> @ {{line}}:{{column}}</span>',
+            replace: true
+        };
+    });
+
+    offendersDirectives.directive('fileAndLineButton', function() {
+        return {
+            restrict: 'E',
+            scope: {
+                file: '=',
+                line: '=',
+                column: '='
+            },
+            template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
+            replace: true
+        };
+    });
+
+})();

+ 2 - 1
front/src/less/rule.less

@@ -120,6 +120,7 @@
             border-bottom-left-radius: 0.4em;
             border-bottom-right-radius: 0.4em;
             border-top: 1px solid #999;
+            z-index: 2;
         }
 
         .domTree {
@@ -140,7 +141,7 @@
             padding: 0.5em;
         }
 
-        &.opens.mouseOver {
+        &.opens:hover {
             border-bottom-left-radius: 0;
             border-bottom-right-radius: 0;
             background: #ffe0cc;

+ 9 - 17
front/src/less/timeline.less

@@ -162,22 +162,6 @@
     text-align: left;
 }
 
-.slowRequestsLimit {
-    width: 3em;
-    font-size: 1em;
-    text-align: right;
-    border: 1px solid #aaa;
-}
-
-input.textFilter {
-    box-shadow: none;
-    font-size: 1em;
-    padding: 0 0.2em;
-    border: 1px solid #aaa;
-    border-radius: none;
-    width: 15em;
-}
-
 .table {
     display: table;
     width: 100%;
@@ -260,6 +244,10 @@ input.textFilter {
     span:hover {
         background: #EBD8E2;
 
+        div {
+            display: inline-block;
+        }
+
         .childArgs {
             display: block;
             position: absolute;
@@ -300,6 +288,7 @@ input.textFilter {
 }
 
 .detailsOverlay {
+    display: none;
     position: absolute;
     right: 3em;
     top: -3em;
@@ -309,13 +298,16 @@ input.textFilter {
     background: #fff;
     border: 2px solid #f1c40f;
     border-radius: 0.5em;
-    z-index: 1;
+    z-index: 2;
 }
 @media screen and (max-width: 1024px) {
     .detailsOverlay {
         width: 25em;
     }
 }
+.showDetails .detailsOverlay {
+    display: block;
+}
 .detailsOverlay .closeBtn {
     position: absolute;
     top: 0.5em;

+ 0 - 1
front/src/main.html

@@ -36,7 +36,6 @@
     <script src="/js/services/apiService.js"></script>
     <script src="/js/services/menuService.js"></script>
     <script src="/js/directives/gradeDirective.js"></script>
-    <script src="/js/directives/jsChildrenDirective.js"></script>
     <script src="/js/directives/offendersDirectives.js"></script>
     <!-- endbuild -->
 <head>

+ 0 - 7
front/src/views/domElementButton.html

@@ -1,7 +0,0 @@
-<div class="offenderButton" ng-class="{opens: obj.tree}">
-    <span ng-if="obj.type == 'html' || obj.type == 'body' || obj.type == 'head' || obj.type == 'window' || obj.type == 'document' || obj.type == 'fragment'"><b>{{obj.type}}</b></span>
-    <span ng-if="obj.type == 'domElement'">DOM element <b>{{obj.element}}</b></span>
-    <span ng-if="obj.type == 'fragmentElement'">Fragment element <b>{{obj.element}}</b></span>
-    <span ng-if="obj.type == 'createdElement'">Created element <b>{{obj.element}}</b></span>
-    <dom-tree ng-if="obj.tree" tree="obj.tree"></dom-tree>
-</div>

+ 1 - 76
front/src/views/timeline.html

@@ -63,15 +63,8 @@
             <input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
             <label for="warningsFilterOn">Show warnings only</label>
         </div>
-        <div>
-            <input type="checkbox" ng-model="textFilterOn" />
-            Filter by
-            <input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
-        </div>
     </div>
     <div class="table">
-
-        <toto data-info="mavariable"></toto>
         <div class="headers">
             <div><!-- index --></div>
             <div>Type</div>
@@ -79,77 +72,9 @@
             <div><!-- details --></div>
             <div>Timestamp</div>
         </div>
-        <div ng-if="(!warningsFilterOn || node.warning || node.error) && (!textFilterOn || !textFilter.length || node.searchIndex.indexOf(textFilter) >= 0)"
-             ng-repeat="node in profilerData" ng-class="{
-                showingDetails: node.showDetails,
-                jsError: node.error,
-                windowPerformance: node.windowPerformance
-             }" id="line_{{$index}}">
-
-            <div class="index">{{$index + 1}}</div>
-            <div class="type">
-                {{node.data.type}}
-                <js-children node="node"></js-children>
-            </div>
-
-            <div class="value">
-                {{node.data.callDetails.arguments[0]}}
-                <span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
-                <span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
-                <span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
-            </div>
-            
-            <div class="details">
-                <div ng-class="{'icon-question': !node.warning && !node.error, 'icon-warning': node.warning || node.error}"
-                     ng-click="onNodeDetailsClick(node)"
-                     ng-if="node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance"></div>
-                
-                <div class="detailsOverlay" ng-if="node.showDetails">
-                    <div class="closeBtn" ng-click="onNodeDetailsClick(node)">✖</div>
-
-                    <div ng-if="node.data.callDetails.context.domElement">
-                        <h4>Called on DOM element</h4>
-                        <div>{{node.data.callDetails.context.domElement}}</div>
-                    </div>
-
-                    <div ng-if="node.data.callDetails.context.length === 0">
-                        <h4>Called on 0 jQuery element</h4>
-                        <p class="advice">Useless function call, as the jQuery object is empty.</p>
-                    </div>
-
-                    <div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
-                        <h4>Called on 1 jQuery element</h4>
-                        <div>{{node.data.callDetails.context.firstElementPath}}</div>
-                    </div>
-
-                    <div ng-if="node.data.callDetails.context.length > 1">
-                        <h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
-                        <p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
-                            The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
-                        </p>
-                        <p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
-                    </div>
 
-                    <p class="advice" ng-if="node.data.resultsNumber === 0">
-                        The query returned 0 results. Could it be unused or dead code?
-                    </p>
+        <profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
 
-                    <div ng-if="node.parsedBacktrace">
-                        <h4>Backtrace</h4>
-                        <div class="table">
-                            <div ng-repeat="trace in node.parsedBacktrace track by $index">
-                                <div>{{trace.fnName || '(anonymous)'}}</div>
-                                <div class="trace"><url-link url="trace.filePath" max-length="40"></url-link>:{{trace.line}}</div>
-                            </div>
-                            <div ng-if="node.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
-                                <div>can't find any backtrace :/</div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-            <div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
-        </div>
     </div>
 
     <div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>

+ 9 - 0
lib/offendersHelpers.js

@@ -32,6 +32,15 @@ var OffendersHelpers = function() {
     };
 
     this.domPathToDomElementObj = function(domPath) {
+
+        if (typeof domPath === 'boolean') {
+            return {
+                // Not a normal element path
+                type: 'notAnElement',
+                element: domPath
+            };
+        }
+
         var domArray = this.domPathToArray(domPath);
         var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
 

+ 27 - 0
lib/tools/jsExecutionTransformer.js

@@ -1,5 +1,7 @@
 var debug = require('debug')('ylt:jsExecutionTransformer');
 
+var offendersHelpers = require('../offendersHelpers');
+
 var jsExecutionTransformer = function() {
 
     this.transform = function(data) {
@@ -46,6 +48,22 @@ var jsExecutionTransformer = function() {
                             data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
                             break;
                     }
+
+                    // Change the list of dom paths into a tree
+                    treeRecursiveParser(node, function(node) {
+                        
+                        if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
+                            node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
+                        }
+
+                        if (node.data.type === 'appendChild' || node.data.type === 'insertBefore') {
+                            node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
+                        }
+
+                        if (node.data.type === 'insertBefore') {
+                            node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
+                        }
+                    });
                 });
             }
 
@@ -57,6 +75,15 @@ var jsExecutionTransformer = function() {
 
         return javascriptExecutionTree;
     };
+
+    function treeRecursiveParser(node, fn) {
+        if (node.children) {
+            node.children.forEach(function(child) {
+                treeRecursiveParser(child, fn);
+            });
+        }
+        fn(node);
+    }
 };
 
 module.exports = new jsExecutionTransformer();

+ 18 - 20
lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js

@@ -33,9 +33,6 @@ exports.module = function(phantomas) {
                     phantomas.enterContext({
                         type: 'getElementById',
                         callDetails: {
-                            context: {
-                                domElement: '#document'
-                            },
                             arguments: ['#' + id]
                         },
                         backtrace: phantomas.getBacktrace()
@@ -65,7 +62,8 @@ exports.module = function(phantomas) {
                         type: 'getElementsByClassName',
                         callDetails: {
                             context: {
-                                domElement: context
+                                length: 1,
+                                elements: [context]
                             },
                             arguments: ['.' + className]
                         },
@@ -103,7 +101,8 @@ exports.module = function(phantomas) {
                         type: 'getElementsByTagName',
                         callDetails: {
                             context: {
-                                domElement: context
+                                length: 1,
+                                elements: [context]
                             },
                             arguments: [tagName]
                         },
@@ -145,7 +144,8 @@ exports.module = function(phantomas) {
                         type: 'querySelector',
                         callDetails: {
                             context: {
-                                domElement: context
+                                length: 1,
+                                elements: [context]
                             },
                             arguments: [selector]
                         },
@@ -177,7 +177,8 @@ exports.module = function(phantomas) {
                         type: 'querySelectorAll',
                         callDetails: {
                             context: {
-                                domElement: context
+                                length: 1,
+                                elements: [context]
                             },
                             arguments: [selector]
                         },
@@ -237,7 +238,8 @@ exports.module = function(phantomas) {
                         type: 'appendChild',
                         callDetails: {
                             context: {
-                                domElement: context
+                                length: 1,
+                                elements: [context]
                             },
                             arguments: [appended]
                         },
@@ -245,20 +247,25 @@ exports.module = function(phantomas) {
                     });
                 }
 
-                function insertBeforeSpyBefore(child) {
+                function insertBeforeSpyBefore(child, refElement) {
                     /*jshint validthis: true */
                     
                     var context = phantomas.getDOMPath(this);
                     var appended = phantomas.getDOMPath(child);
+                    var referent = phantomas.getDOMPath(refElement);
                     appendChild(child, this, context, appended);
 
                     phantomas.enterContext({
                         type: 'insertBefore',
                         callDetails: {
                             context: {
-                                domElement: context
+                                length: 1,
+                                elements: [context]
                             },
-                            arguments: [appended]
+                            arguments: [
+                                appended,
+                                referent
+                            ]
                         },
                         backtrace: phantomas.getBacktrace()
                     });
@@ -277,9 +284,6 @@ exports.module = function(phantomas) {
                     phantomas.enterContext({
                         type: 'createElement',
                         callDetails: {
-                            context: {
-                                domElement: '#document'
-                            },
                             arguments: [tagName]
                         },
                         backtrace: phantomas.getBacktrace()
@@ -295,9 +299,6 @@ exports.module = function(phantomas) {
                     phantomas.enterContext({
                         type: 'createTextNode',
                         callDetails: {
-                            context: {
-                                domElement: '#document'
-                            },
                             arguments: [text]
                         },
                         backtrace: phantomas.getBacktrace()
@@ -313,9 +314,6 @@ exports.module = function(phantomas) {
                     phantomas.enterContext({
                         type: 'createDocumentFragment',
                         callDetails: {
-                            context: {
-                                domElement: '#document'
-                            },
                             arguments: []
                         },
                         backtrace: phantomas.getBacktrace()

+ 2 - 1
lib/tools/phantomas/custom_modules/modules/eventListYLT/eventListYLT.js

@@ -27,7 +27,8 @@ exports.module = function(phantomas) {
                         type: 'addEventListener',
                         callDetails: {
                             context: {
-                                domElement: path
+                                length: 1,
+                                elements: [path]
                             },
                             arguments: [eventType]
                         },

+ 77 - 21
lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js

@@ -68,7 +68,7 @@ exports.module = function(phantomas) {
         'slideUp',
         'slideToggle',
 
-        // generic events
+        // Generic events
         'on',
         'off',
         'live',
@@ -79,7 +79,7 @@ exports.module = function(phantomas) {
         'bind',
         'unbind',
 
-        // more events
+        // More events
         'blur',
         'change',
         'click',
@@ -106,7 +106,7 @@ exports.module = function(phantomas) {
         'submit',
         'unload',
 
-        // attributes
+        // Attributes
         'attr',
         'prop',
         'removeAttr',
@@ -115,12 +115,31 @@ exports.module = function(phantomas) {
         'hasClass',
         'addClass',
         'removeClass',
-        'toggleClass'
+        'toggleClass',
     ];
 
+    var jQueryTraversalFunctions = [
+        'children',
+        'closest',
+        'find',
+        'next',
+        'nextAll',
+        'nextUntil',
+        'offsetParent',
+        'parent',
+        'parents',
+        'parentsUntil',
+        'prev',
+        'prevAll',
+        'prevUntil',
+        'siblings'
+    ];
+
+    jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
+
     // spy calls to jQuery functions
     phantomas.once('init', function() {
-        phantomas.evaluate(function(jQueryFunctions) {
+        phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
             (function(phantomas) {
                 var jQuery;
                 var oldJQuery;
@@ -178,11 +197,11 @@ exports.module = function(phantomas) {
                         phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
                         
                         phantomas.enterContext({
-                            type: 'jQuery - find',
+                            type: 'jQuery - Sizzle call',
                             callDetails: {
                                 context: {
-                                    length: this.length,
-                                    firstElementPath: phantomas.getDOMPath(context)
+                                    length: 1,
+                                    elements: [phantomas.getDOMPath(context)]
                                 },
                                 arguments: [selector]
                             },
@@ -196,6 +215,36 @@ exports.module = function(phantomas) {
                         phantomas.leaveContext(moreData);
                     }) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
 
+
+                    phantomas.spy(jQuery.fn, 'init', function(selector, context) {
+                        if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) {
+
+                            phantomas.enterContext({
+                                type: 'jQuery - find',
+                                callDetails: {
+                                    arguments: [selector]
+                                },
+                                backtrace: phantomas.getBacktrace()
+                            });
+
+                        }
+
+                    }, function(result) {
+                        var data = phantomas.getContextData();
+
+                        if (data.type === 'jQuery - find' &&
+                                !data.callDetails.context &&
+                                data.callDetails.arguments.length === 1 &&
+                                /^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) {
+
+                            var moreData = {
+                                resultsNumber : (result && result.length) ? result.length : 0
+                            };
+                            phantomas.leaveContext(moreData);
+                        }
+                    });
+
+
                     /*if (!jQuery.event) {
                         phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
                             var path = phantomas.getDOMPath(elem),
@@ -213,20 +262,11 @@ exports.module = function(phantomas) {
                         if ((eventName === 'load') && (this[0] === window)) {
                             phantomas.incrMetric('jQueryWindowOnLoadFunctions');
                             phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
-
-                            phantomas.pushContext({
-                                type: 'jQuery - windowOnLoad',
-                                callDetails: {
-                                    arguments: [func]
-                                },
-                                backtrace: phantomas.getBacktrace()
-                            });
                         }
                     });
 
                     // Add spys on many jQuery functions
                     jQueryFunctions.forEach(function(functionName) {
-                        var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
                         
                         phantomas.spy(jQuery.fn, functionName, function(args) {
 
@@ -234,6 +274,11 @@ exports.module = function(phantomas) {
                             args = [].slice.call(arguments);
                             args.forEach(function(arg, index) {
                                 
+                                
+                                if (arg instanceof Array) {
+                                    arg = '[Array]';
+                                }
+
                                 if (arg instanceof Object) {
                                     
                                     if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
@@ -286,13 +331,17 @@ exports.module = function(phantomas) {
                                 args[index] = arg;
                             });
 
+                            var elements = [];
+                            for (var i = 0 ; i < this.length ; i++) {
+                                elements.push(phantomas.getDOMPath(this[i]));
+                            }
 
                             phantomas.enterContext({
                                 type: 'jQuery - ' + functionName,
                                 callDetails: {
                                     context: {
                                         length: this.length,
-                                        firstElementPath: phantomas.getDOMPath(this[0])
+                                        elements: elements
                                     },
                                     arguments: args
                                 },
@@ -300,12 +349,19 @@ exports.module = function(phantomas) {
                             });
 
                         }, function(result) {
-                            phantomas.leaveContext();
-                        }) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)');
+                            if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
+                                var moreData = {
+                                    resultsNumber : (result && result.length) ? result.length : 0
+                                };
+                                phantomas.leaveContext(moreData);
+                            } else {
+                                phantomas.leaveContext();
+                            }
+                        }) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
                     });
                 });
             })(window.__phantomas);
-        }, jQueryFunctions);
+        }, jQueryFunctions, jQueryTraversalFunctions);
     });
 
 

+ 193 - 0
test/www/jquery-page.html

@@ -0,0 +1,193 @@
+<html>
+<head>
+    <title>Simple page</title>
+    <script src="jquery1.8.3.js"></script>
+</head>
+<body>
+    <h1>Simple page</h1>
+
+    <ul>
+        <li>li 1</li>
+        <li>li 2</li>
+        <li>li 3</li>
+    </ul>
+
+    <ul id="foo">
+        <li>li4</li>
+    </ul>
+
+    <script>
+
+        // Let's start the spaghetti code!!!
+
+        $('ul').find('li');
+        $('li', $('#foo'));
+        var foo = document.getElementById('foo');
+        $('li', foo);
+        foo.getElementsByClassName('bar');
+        document.getElementsByClassName('bar');
+        foo.querySelector('ul li');
+        document.querySelector('ul li');
+        foo.querySelectorAll('ul li');
+        document.querySelectorAll('ul li');
+
+        var li = document.createElement('li');
+        foo.insertBefore(li, foo.children[0]);
+
+        li.addEventListener('click', function() {});
+
+        var $li = $(li);
+        $li.html();
+        $li.html('some content');
+        $('li').html();
+        $li.html('');
+        $('select').html();
+
+        $li.append('<div>woot</div>');
+        $li.append(document.createElement('span'));
+        $li.append($('<b>what</b>'));
+        $li.append(['<div>test1</div>', document.createElement('div')]);
+        $li.append('<div>test3</div>', '<div>test4</div>');
+
+        $('<div>test4</div>').appendTo($li);
+        $('<div>test5</div>').appendTo('<div class="bar"></div>');
+
+        $('li').prepend('<span class="prepended">prepended!</span>');
+        $('<div>test0</div>').prependTo($li);
+
+        $('<div>before bar</div>').before('.bar');
+        $('<div>before $li</div>').insertBefore($li);
+
+        $li.after('textnode after');
+        $('<div>after $li</div>').insertAfter($li);
+
+        $('.prepended').remove('.someClass');
+        $('.prepended').remove();
+        $('.prepended').detach();
+
+        $li.find('div').empty();
+        var $ulClones = $('ul').clone();
+
+        $('span').replaceWith('<span class="newspan">New span</span>');
+        $('<span class="newnewspan">New new span</span>').replaceAll('.newspan');
+
+        $('li').text();
+        $('li').eq(0).text('New li 1');
+
+        $('li').eq(0).wrap('<div></div>');
+        $('ul').wrapAll('<div class="ulWrapper"></div>');
+        $('.ulWrapper').wrapInner('<div class="doubleUlWrapper"></div>');
+        $('.doubleUlWrapper').unwrap();
+
+        $li.css('color', '#F00');
+        $li.css('color');
+        $li.css({'font-size': '1.2em', 'background': '#BFF'});
+        $li.offset();
+        $li.offset({left: 12, top: 23});
+        $li.offset(function() {return {left: 55, top: 44};});
+        $li.height();
+
+        $li.hide();
+        $li.hide(120);
+        $li.hide({duration: 133, done: function() {console.log(12);}});
+        $li.hide(222, 'swing', function() {console.log(9);});
+        $li.toggle();
+        $li.toggle(true);
+        $li.toggle(false);
+        $li.toggle(200);
+
+        var handler = function() {};
+        var $tbody = $('#dataTable tbody');
+        var $tbodytr = $('#dataTable tbody tr');
+        $tbodytr.on('click', handler);
+        $tbody.on('click', 'tr', handler);
+        $tbody.on('click', {someData: 'Joe'}, handler);
+        $tbodytr.off('click', handler);
+        $tbodytr.off('click', 'tr');
+        $tbodytr.off('click');
+        $tbodytr.off();
+        $tbody.delegate('tr', 'click', handler);
+        $tbody.undelegate('tr', 'click');
+        $tbody.undelegate('someNamespage');
+        $tbody.undelegate();
+        $tbody.one('zoop', handler);
+        $tbody.bind('zoop', handler);
+        $tbody.unbind('zoop', handler);
+        $tbody.bind();
+
+        $tbody.blur(handler);
+        $tbody.change(handler);
+        $tbody.click(handler);
+        $tbody.dblclick(handler);        
+        $tbody.error(handler);
+        $tbody.focus(handler);
+        $tbody.focusin(handler);
+        $tbody.focusout(handler);
+        $tbody.hover(handler);
+        $tbody.keydown(handler);
+        $tbody.keypress(handler);
+        $tbody.keyup(handler);
+        $tbody.load(handler);
+        $tbody.mousedown(handler);
+        $tbody.mouseenter(handler);
+        $tbody.mouseleave(handler);
+        $tbody.mousemove(handler);
+        $tbody.mouseout(handler);
+        $tbody.mouseover(handler);
+        $tbody.mouseup(handler);
+        $tbody.resize(handler);
+        $tbody.scroll(handler);
+        $tbody.select(handler);
+        $tbody.submit(handler);
+        $tbody.unload(handler);
+
+        $tbody.attr({alt: "Beijing Brush Seller", title: "photo by Kelly Clark"});
+        $tbody.attr('alt');
+        $tbody.attr('alt', 'An alternative alt');
+        $tbody.attr('alt', '');
+        $tbody.removeAttr('alt');
+
+        $tbody.prop({checked: true});
+        $tbody.prop('checked');
+        $tbody.prop('checked', true);
+        $tbody.prop('checked', '');
+        $tbody.removeProp('checked');
+
+        var $input = $('<input type="text"></input>');
+        $input.val('some value');
+        $input.val();
+
+        $tbody.addClass(function() {return 'foo';});
+        $tbody.hasClass('foo');
+        $tbody.removeClass('foo');
+        $tbody.toggleClass('foo');
+        $tbody.toggleClass('foo', true);
+        $tbody.toggleClass();
+
+        $li.children();
+        $li.children('span');
+        $li.closest('ul');
+        $li.closest('ul', document.getElementById('foo'));
+        $li.find('span');
+        $li.find(document.getElementById('foo'));
+        $li.find($li);
+        $li.next();
+        $li.next('li');
+        $li.nextAll();
+        $li.nextAll('li');
+        $li.nextUntil('.theEnd', 'li');
+        $li.offsetParent();
+        $li.prevAll();
+        $li.prevAll('li');
+        $li.prevUntil('.theBeginning', 'li');
+        $li.parent();
+        $li.parent('ul');
+        $li.parents();
+        $li.parents('div');
+        $li.parentsUntil('ul');
+        $li.parentsUntil('body', 'div');
+        $li.siblings();
+        $li.siblings('li');
+    </script>
+</body>
+</html>

File diff suppressed because it is too large
+ 1 - 0
test/www/jquery1.8.3.js


Some files were not shown because too many files changed in this diff