reorganize plugin

test-unit-sauce
Brian Ford 11 years ago
parent ee422a1957
commit 704ac4dfbb

1
.gitignore vendored

@ -1 +1,2 @@
batarang-release-*.zip
*.build.js

@ -6,10 +6,12 @@ https://chrome.google.com/webstore/detail/ighdmehidhipcmcojjgiloacoafjmpfk
## Installing from Source
1. Clone the repository: `git clone git://github.com/angular/angularjs-batarang`
2. Navigate to `chrome://chrome/extensions/` and enable Developer Mode.
3. Choose "Load unpacked extension"
4. Open the directory you just cloned (should open with Chrome, otherwise try dragging/dropping the file into Chrome) and follow the prompts to install.
1. Clone the repository: `git clone git://github.com/angular/angularjs-batarang`
2. Install the bower dependencies: `bower install`
3. Build the inject script: `node scripts/inline.js`
4. Navigate to `chrome://chrome/extensions/` and enable Developer Mode.
5. Choose "Load unpacked extension"
6. In the dialog, open the directory you just cloned.
## Screencast

@ -1,5 +1,5 @@
<html>
<body>
<script src="js/background.js"></script>
<script src="background.js"></script>
</body>
</html>

@ -36,6 +36,10 @@ chrome.extension.onConnect.addListener(function (port) {
}
}
if (!scopeCache[msg.appId]) {
scopeCache[msg.appId] = {};
}
// immediately populate the scopes tree from the cache
Object.keys(scopeCache[msg.appId]).forEach(function (scopeId) {
port.postMessage({ // [background] --> [devtools]

@ -9,4 +9,4 @@
"devDependencies": {
"angular-mocks": "~1.0.7"
}
}
}

@ -0,0 +1,20 @@
# content-scripts/inject.js
What does this do?
This adds a script to the page that watches DOM mutation events until it sees that `window.angular` is available.
Immediately after, before any applications have the opportunity to bootstrap, this decorates core Angular
components to expose debugging information.
## Building debug.js
Why does this need a build step?
Because this script does `fn.toString()` to construct the script tags, it's impossible to use any sort of
code loading. The code needs to be inlined before being run.
From the root of this repository, run:
```shell
node ./scripts/inline.js
```

@ -0,0 +1,356 @@
// content-scripts/inject.js
// this file is run from the content script context (separate JS VM from the app, but same DOM)
// but injects an 'instrumentation' script tag into the app context
// confusing, right?
var instument = function instument (window) {
// Helper to determine if the root 'ng' module has been loaded
// window.angular may be available if the app is bootstrapped asynchronously, but 'ng' might
// finish loading later.
var ngLoaded = function () {
if (!window.angular) {
return false;
}
try {
window.angular.module('ng');
}
catch (e) {
return false;
}
return true;
};
if (!ngLoaded()) {
// TODO: var name
var areWeThereYet = function (ev) {
if (ev.srcElement.tagName === 'SCRIPT') {
var oldOnload = ev.srcElement.onload;
ev.srcElement.onload = function () {
if (ngLoaded()) {
document.removeEventListener('DOMNodeInserted', areWeThereYet);
instument(window);
}
if (oldOnload) {
oldOnload.apply(this, arguments);
}
};
}
};
document.addEventListener('DOMNodeInserted', areWeThereYet);
return;
}
// do not patch twice
if (window.__ngDebug) {
return;
}
// Helpers
// =======
var throttle = require('./lib/throttle.js');
var summarizeObject = require('./lib/summarizeObject.js');
// helper to extract dependencies from function arguments
// not all versions of AngularJS expose annotate
var annotate = angular.injector().annotate || require('./lib/annotate.js');
// polyfill for performance.now on older webkit
if (!performance.now) {
performance.now = performance.webkitNow;
}
// Send notifications from app context to devtools context
// in order to do this, we need to create a DOM element across which
// the app and content script contexts can communicate
var eventProxyElement = document.createElement('div');
eventProxyElement.id = '__ngDebugElement';
eventProxyElement.style.display = 'none';
document.body.appendChild(eventProxyElement);
var customEvent = document.createEvent('Event');
customEvent.initEvent('myCustomEvent', true, true);
var fireCustomEvent = function (data) {
data.appId = instrumentedAppId;
eventProxyElement.innerText = JSON.stringify(data);
eventProxyElement.dispatchEvent(customEvent);
};
// given a scope object, return an object with deep clones
// of the models exposed on that scope
var getScopeLocals = function (scope) {
var scopeLocals = {}, prop;
for (prop in scope) {
if (scope.hasOwnProperty(prop) && prop !== 'this' && prop[0] !== '$') {
scopeLocals[prop] = decycle(scope[prop]);
}
}
return scopeLocals;
};
// Private state
// =============
//var bootstrap = window.angular.bootstrap;
var debug = {
// map of scopes --> watcher function name strings
watchers: {},
// maps of watch/apply exp/fns to perf data
watchPerf: {},
applyPerf: {},
// map of scope.$ids --> $scope objects
scopes: {},
// whether or not to emit profile data
profiling: false,
// map of $ids --> [] array of things being watched
modelWatchers: {},
// map of $id + watcher --> value
modelWatchersState: {},
// map of $ids --> refs to $rootScope objects
rootScopes: {},
deps: []
};
var popover = null;
var instrumentedAppId = window.location.host + '~' + Math.random();
// Utils
// =====
var getScopeTree = function (id) {
var names = api.niceNames();
var traverse = function (sc) {
var tree = {
id: sc.$id,
name: names[sc.$id],
watchers: debug.watchers[sc.$id],
children: []
};
var child = sc.$$childHead;
if (child) {
do {
tree.children.push(traverse(child));
} while (child !== sc.$$childTail && (child = child.$$nextSibling));
}
return tree;
};
var root = debug.rootScopes[id];
var tree = traverse(root);
return tree;
};
var getWatchPerf = function () {
var changes = [];
angular.forEach(debug.watchPerf, function (info, name) {
if (info.time > 0) {
changes.push({
name: name,
time: info.time
});
info.time = 0;
}
});
return changes;
};
// Emit stuff
// ==========
var emit = {
modelChange: throttle(function (id, watchers) {
var scope = debug.scopes[id];
var changes = {};
watchers = watchers || debug.modelWatchers[id];
if (scope && debug.modelWatchers[id]) {
Object.keys(debug.modelWatchers[id]).
forEach(function (watcher) {
var newValue = api.getModel(id, watcher),
newString = JSON.stringify(newValue),
prop = id + '~' + watcher;
if (debug.modelWatchersState[prop] !== newString) {
changes[watcher] = newValue;
debug.modelWatchersState[prop] = newString;
}
});
}
if (Object.keys(changes).length > 0) {
fireCustomEvent({
action: 'modelChange',
id: id,
changes: changes
});
}
}, 50),
scopeChange: throttle(function (id) {
fireCustomEvent({
action: 'scopeChange',
id: id,
scope: getScopeTree(id)
});
}, 50),
scopeDeleted: function (id) {
fireCustomEvent({
action: 'scopeDeleted',
id: id
});
},
watcherChange: throttle(function (id) {
if (debug.modelWatchers[id]) {
fireCustomEvent({
action: 'watcherChange',
id: id,
watchers: debug.watchers[id]
});
}
}, 50),
watchPerfChange: throttle(function (str) {
if (debug.profiling) {
fireCustomEvent({
action: 'watchPerfChange',
watcher: str,
value: debug.watchPerf[str]
});
}
}, 50),
applyPerfChange: throttle(function (str) {
if (debug.profiling) {
fireCustomEvent({
action: 'applyPerfChange',
watcher: str,
value: debug.applyPerf[str]
});
}
}, 50),
// might be worth limiting
watchPerf: function () {
throw new Error('Implement me :c');
}
};
// Public API
// ==========
var api = window.__ngDebug = {
profiling: function (setting) {
debug.profiling = setting;
},
getDeps: function () {
return debug.deps;
},
getRootScopeIds: function () {
return Object.keys(debug.rootScopes);
},
getAppId: function () {
return instrumentedAppId;
},
fireCustomEvent: fireCustomEvent,
niceNames: require('./lib/niceNames.js'),
getModel: require('./lib/summarizeModel.js'),
setSomeModel: function (id, path, value) {
debug.scope[id].$apply(path + '=' + JSON.stringify(value));
},
watchModel: function (id, path) {
debug.modelWatchers[id] = debug.modelWatchers[id] || {};
debug.modelWatchers[id][path || ''] = true;
if (!path || path === '') {
debug.modelWatchersState = {};
}
emit.modelChange(id);
emit.watcherChange(id);
},
// unwatches all children of the given path
// Ex:
// if watching 'foo.bar.baz', 'foo.bar', and 'foo'
// unwatchModel('001', 'foo.bar')
// unwatches 'foo.bar.baz' and 'foo.bar'
unwatchModel: function (id, path) {
if (!debug.modelWatchers[id]) {
return;
}
if (path === undefined) {
path = '';
}
Object.keys(modelWatchers[id]).forEach(function (key) {
if (key.substr(0, path.length) === path) {
delete debug.modelWatchers[id][key];
}
});
},
enable: require('./lib/popover.js')
};
var recordDependencies = function (providerName, dependencies) {
debug.deps.push({
name: providerName,
imports: dependencies
});
};
require('./lib/decorate.js');
};
// inject into the application context from the content script context
var inject = function () {
var script = window.document.createElement('script');
script.innerHTML = '(' + instument.toString() + '(window))';
document.head.appendChild(script);
// handle forwarding the events sent from the app context to the
// background page context
var eventProxyElement = document.getElementById('__ngDebugElement');
if (eventProxyElement) {
eventProxyElement.addEventListener('myCustomEvent', function () {
var eventData = JSON.parse(eventProxyElement.innerText);
chrome.extension.sendMessage(eventData);
});
document.removeEventListener('DOMContentLoaded', inject);
}
};
// only inject if cookie is set
if (document.cookie.indexOf('__ngDebug=true') != -1) {
document.addEventListener('DOMContentLoaded', inject);
}

@ -0,0 +1,51 @@
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(.+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
// TODO: should I keep these assertions?
function assertArg(arg, name, reason) {
if (!arg) {
throw new Error("Argument '" + (name || '?') + "' is " + (reason || "required"));
}
return arg;
}
function assertArgFn(arg, name, acceptArrayAnnotation) {
if (acceptArrayAnnotation && angular.isArray(arg)) {
arg = arg[arg.length - 1];
}
assertArg(angular.isFunction(arg), name, 'not a function, got ' +
(arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg));
return arg;
}
module.exports = function (fn) {
var $inject,
fnText,
argDecl,
last;
if (typeof fn == 'function') {
if (!($inject = fn.$inject)) {
$inject = [];
fnText = fn.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS);
argDecl[1].split(FN_ARG_SPLIT).forEach(function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(name);
});
});
fn.$inject = $inject;
}
} else if (angular.isArray(fn)) {
last = fn.length - 1;
assertArgFn(fn[last], 'fn');
$inject = fn.slice(0, last);
} else {
assertArgFn(fn, 'fn', true);
}
return $inject;
};

@ -0,0 +1,251 @@
var ng = angular.module('ng');
ng.config(function ($provide) {
// methods to patch
// $provide.provider
var temp = $provide.provider;
$provide.provider = function (name, definition) {
if (!definition) {
angular.forEach(name, function (definition, name) {
var tempGet = definition.$get;
definition.$get = function () {
recordDependencies(name, annotate(tempGet));
return tempGet.apply(this, arguments);
};
});
} else if (definition instanceof Array) {
// it is a constructor with array syntax
var tempConstructor = definition[definition.length - 1];
definition[definition.length - 1] = function () {
recordDependencies(name, annotate(tempConstructor));
return tempConstructor.apply(this, arguments);
};
} else if (definition.$get instanceof Array) {
// it should have a $get
var tempGet = definition.$get[definition.$get.length - 1];
definition.$get[definition.$get.length - 1] = function () {
recordDependencies(name, annotate(tempGet));
return tempGet.apply(this, arguments);
};
} else if (typeof definition === 'object') {
// it should have a $get
var tempGet = definition.$get;
// preserve original annotations
definition.$get = annotate(definition.$get);
definition.$get.push(function () {
recordDependencies(name, annotate(tempGet));
return tempGet.apply(this, arguments);
});
} else {
recordDependencies(name, annotate(definition));
}
return temp.apply(this, arguments);
};
// $provide.(factory|service)
[
'factory',
'service'
].forEach(function (met) {
var temp = $provide[met];
$provide[met] = function (name, definition) {
if (typeof name === 'object') {
angular.forEach(name, function (value, key) {
var isArray = value instanceof Array;
var originalValue = isArray ? value[value.length - 1] : value;
var newValue = function () {
recordDependencies(key, annotate(originalValue));
return originalValue.apply(this, arguments);
};
if (isArray) {
value[value.length - 1] = newValue;
} else {
name[value] = newValue;
}
});
} else {
recordDependencies(name, annotate(definition));
}
return temp.apply(this, arguments);
};
});
$provide.decorator('$rootScope', function ($delegate) {
var watchFnToHumanReadableString = function (fn) {
if (fn.exp) {
return fn.exp.trim();
}
if (fn.name) {
return fn.name.trim();
}
return fn.toString();
};
var applyFnToLogString = function (fn) {
var str;
if (fn) {
if (fn.name) {
str = fn.name;
} else if (fn.toString().split('\n').length > 1) {
str = 'fn () { ' + fn.toString().split('\n')[1].trim() + ' /* ... */ }';
} else {
str = fn.toString().trim().substr(0, 30) + '...';
}
} else {
str = '$apply';
}
return str;
};
// patch registering watchers
// ==========================
var _watch = $delegate.__proto__.$watch;
$delegate.__proto__.$watch = function (watchExpression, applyFunction) {
var thatScope = this;
var watchStr = watchFnToHumanReadableString(watchExpression);
if (!debug.watchPerf[watchStr]) {
debug.watchPerf[watchStr] = {
time: 0,
calls: 0
};
}
if (!debug.watchers[thatScope.$id]) {
debug.watchers[thatScope.$id] = [];
}
debug.watchers[thatScope.$id].push(watchStr);
emit.watcherChange(thatScope.$id);
// patch watchExpression
// ---------------------
var w = watchExpression;
if (typeof w === 'function') {
watchExpression = function () {
var start = performance.now();
var ret = w.apply(this, arguments);
var end = performance.now();
debug.watchPerf[watchStr].time += (end - start);
debug.watchPerf[watchStr].calls += 1;
emit.watchPerfChange(watchStr);
return ret;
};
} else {
watchExpression = function () {
var start = performance.now();
var ret = thatScope.$eval(w);
var end = performance.now();
debug.watchPerf[watchStr].time += (end - start);
debug.watchPerf[watchStr].calls += 1;
emit.watchPerfChange(watchStr);
return ret;
};
}
// patch applyFunction
// -------------------
if (typeof applyFunction === 'function') {
var applyStr = applyFunction.toString();
var unpatchedApplyFunction = applyFunction;
applyFunction = function () {
var start = performance.now();
var ret = unpatchedApplyFunction.apply(this, arguments);
var end = performance.now();
//TODO: move these checks out of here and into registering the watcher
if (!debug.applyPerf[applyStr]) {
debug.applyPerf[applyStr] = {
time: 0,
calls: 0
};
}
debug.applyPerf[applyStr].time += (end - start);
debug.applyPerf[applyStr].calls += 1;
emit.applyPerfChange(applyStr);
return ret;
};
}
return _watch.apply(this, arguments);
};
// patch $destroy
// --------------
var _destroy = $delegate.__proto__.$destroy;
$delegate.__proto__.$destroy = function () {
[
'watchers',
'scopes'
].forEach(function (prop) {
if (debug[prop][this.$id]) {
delete debug[prop][this.$id];
}
}, this);
emit.scopeDeleted(this.$id);
return _destroy.apply(this, arguments);
};
// patch $new
// ----------
var _new = $delegate.__proto__.$new;
$delegate.__proto__.$new = function () {
var ret = _new.apply(this, arguments);
if (ret.$root) {
debug.rootScopes[ret.$root.$id] = ret.$root;
emit.scopeChange(ret.$root.$id);
}
// create empty watchers array for this scope
if (!debug.watchers[ret.$id]) {
debug.watchers[ret.$id] = [];
}
debug.scopes[ret.$id] = ret;
debug.scopes[this.$id] = this;
return ret;
};
// patch $digest
// -------------
var _digest = $delegate.__proto__.$digest;
$delegate.__proto__.$digest = function (fn) {
var ret = _digest.apply(this, arguments);
emit.modelChange(this.$id);
return ret;
};
// patch $apply
// ------------
var _apply = $delegate.__proto__.$apply;
$delegate.__proto__.$apply = function (fn) {
var start = performance.now();
var ret = _apply.apply(this, arguments);
var end = performance.now();
// If the debugging option is enabled, log to console
// --------------------------------------------------
if (debug.log) {
console.log(applyFnToLogString(fn) + '\t\t' + (end - start).toPrecision(4) + 'ms');
}
return ret;
};
return $delegate;
});
});

@ -0,0 +1,58 @@
module.exports = function niceNames () {
var ngScopeElts = document.getElementsByClassName('ng-scope');
ngScopeElts = Array.prototype.slice.call(ngScopeElts);
return ngScopeElts.
reduce(function (acc, elt) {
var $elt = angular.element(elt);
var scope = $elt.scope();
var name = {};
[
'ng-app',
'ng-controller',
'ng-repeat'
].
forEach(function (attr) {
var val = $elt.attr(attr),
className = $elt[0].className,
match,
lhs,
valueIdentifier,
keyIdentifier;
if (val) {
name[attr] = val;
if (attr === 'ng-repeat') {
match = /(.+) in/.exec(val);
lhs = match[1];
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
valueIdentifier = match[3] || match[1];
keyIdentifier = match[2];
if (keyIdentifier) {
name.lhs = valueIdentifier + '["' + scope[keyIdentifier] + '"]' + summarizeObject(scope[valueIdentifier]);
} else {
name.lhs = valueIdentifier + summarizeObject(scope[valueIdentifier]);
}
}
} else if (className.indexOf(attr) !== -1) {
match = (new RegExp(attr + ': ([a-zA-Z0-9]+);')).exec(className);
name[attr] = match[1];
}
});
if (Object.keys(name).length === 0) {
name.tag = $elt[0].tagName.toLowerCase();
name.classes = $elt[0].className.
replace(/(\W*ng-scope\W*)/, ' ').
split(' ').
filter(function (i) { return i; });
}
acc[scope.$id] = name;
return acc;
}, {});
};

@ -0,0 +1,234 @@
// TODO: this depends on global state and stuff
module.exports = function () {
if (popover) {
return;
}
var angular = window.angular;
popover = angular.element(
'<div style="position: fixed; left: 50px; top: 50px; z-index: 9999; background-color: #f1f1f1; box-shadow: 0 15px 10px -10px rgba(0, 0, 0, 0.5), 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;">' +
'<div style="position: relative" style="min-width: 300px; min-height: 100px;">' +
'<div style="min-width: 100px; min-height: 50px; padding: 5px;"><pre>{ Please select a scope }</pre></div>' +
'<button style="position: absolute; top: -15px; left: -15px; cursor: move;">⇱</button>' +
'<button style="position: absolute; top: -15px; left: 10px;">+</button>' +
'<button style="position: absolute; top: -15px; right: -15px;">x</button>' +
'<style>' +
'.ng-scope.bat-selected { border: 1px solid red; } ' +
'.bat-indent { margin-left: 20px; }' +
'</style>' +
'</div>' +
'</div>');
angular.element(window.document.body).append(popover);
var popoverContent = angular.element(angular.element(popover.children('div')[0]).children()[0]);
var dragElt = angular.element(angular.element(popover.children('div')[0]).children()[1]);
var selectElt = angular.element(angular.element(popover.children('div')[0]).children()[2]);
var closeElt = angular.element(angular.element(popover.children('div')[0]).children()[3]);
var currentScope = null,
currentElt = null;
function onMove (ev) {
var x = ev.clientX,
y = ev.clientY;
if (x > window.outerWidth - 100) {
x = window.outerWidth - 100;
} else if (x < 0) {
x = 0;
}
if (y > window.outerHeight - 100) {
y = window.outerHeight - 100;
} else if (y < 0) {
y = 0;
}
x += 5;
y += 5;
popover.css('left', x + 'px');
popover.css('top', y + 'px');
}
closeElt.bind('click', function () {
popover.remove();
popover = null;
});
selectElt.bind('click', bindSelectScope);
var selecting = false;
function bindSelectScope () {
if (selecting) {
return;
}
setTimeout(function () {
selecting = true;
selectElt.attr('disabled', true);
angular.element(document.body).css('cursor', 'crosshair');
angular.element(document.getElementsByClassName('ng-scope'))
.bind('click', onSelectScope)
.bind('mouseover', onHoverScope);
}, 30);
}
var hoverScopeElt = null;
function markHoverElt () {
if (hoverScopeElt) {
hoverScopeElt.addClass('bat-selected');
}
}
function unmarkHoverElt () {
if (hoverScopeElt) {
hoverScopeElt.removeClass('bat-selected');
}
}
function onSelectScope (ev) {
render(this);
angular.element(document.getElementsByClassName('ng-scope'))
.unbind('click', onSelectScope)
.unbind('mouseover', onHoverScope);
unmarkHoverElt();
selecting = false;
selectElt.attr('disabled', false);
angular.element(document.body).css('cursor', '');
hovering = false;
}
var hovering = false;
function onHoverScope (ev) {
if (hovering) {
return;
}
hovering = true;
var that = this;
setTimeout(function () {
unmarkHoverElt();
hoverScopeElt = angular.element(that);
markHoverElt();
hovering = false;
render(that);
}, 100);
}
function onUnhoverScope (ev) {
angular.element(this).css('border', '');
}
dragElt.bind('mousedown', function (ev) {
ev.preventDefault();
rendering = true;
angular.element(document).bind('mousemove', onMove);
});
angular.element(document).bind('mouseup', function () {
angular.element(document).unbind('mousemove', onMove);
setTimeout(function () {
rendering = false;
}, 120);
});
function renderTree (data) {
var tree = angular.element('<div class="bat-indent"></div>');
angular.forEach(data, function (val, key) {
var toAppend;
if (val === undefined) {
toAppend = '<i>undefined</i>';
} else if (val === null) {
toAppend = '<i>null</i>';
} else if (val instanceof Array) {
toAppend = '[ ... ]';
} else if (val instanceof Object) {
toAppend = '{ ... }';
} else {
toAppend = val.toString();
}
if (data instanceof Array) {
toAppend = '<div>' +
toAppend +
((key === (data.length - 1))?'':',') +
'</div>';
} else {
toAppend = '<div>' +
key +
': ' +
toAppend +
(key!==0?'':',') +
'</div>';
}
toAppend = angular.element(toAppend);
if (val instanceof Array || val instanceof Object) {
function recur () {
toAppend.unbind('click', recur);
toAppend.html('');
toAppend
.append(angular.element('<span>' +
key + ': ' +
((val instanceof Array)?'[':'{') +
'<span>').bind('click', collapse))
.append(renderTree(val))
.append('<span>' + ((val instanceof Array)?']':'}') + '<span>');
}
function collapse () {
toAppend.html('');
toAppend.append(angular.element('<div>' +
key +
': ' +
((val instanceof Array)?'[ ... ]':'{ ... }') +
'</div>').bind('click', recur));
}
toAppend.bind('click', recur);
}
tree.append(toAppend);
});
return tree;
}
var isEmpty = function (object) {
var prop;
for (prop in object) {
if (object.hasOwnProperty(prop)) {
return false;
}
}
return true;
};
var objLength = function (object) {
var prop, len = 0;
for (prop in object) {
if (object.hasOwnProperty(prop)) {
len += 1;
}
}
return len;
};
var rendering = false;
var render = function (elt) {
if (rendering) {
return;
}
rendering = true;
setTimeout(function () {
var scope = angular.element(elt).scope();
rendering = false;
if (scope === currentScope) {
return;
}
currentScope = scope;
currentElt = elt;
var models = getScopeLocals(scope);
popoverContent.children().remove();
if (isEmpty(models)) {
popoverContent.append(angular.element('<i>This scope has no models</i>'));
} else {
popoverContent.append(renderTree(models));
}
}, 100);
};
};

@ -0,0 +1,51 @@
// TODO: handle DOM nodes, fns, etc better.
var subModel = function (obj) {
return obj instanceof Array ?
{ '~array-length': obj.length } :
obj === null ?
null :
typeof obj === 'object' ?
{ '~object': true } :
obj;
};
module.exports = function (id, path) {
if (path === undefined || path === '') {
path = [];
} else if (typeof path === 'string') {
path = path.split('.');
}
var dest = debug.scopes[id],
segment;
if (!dest) {
return;
}
while (path.length > 0) {
segment = path.shift();
dest = dest[segment];
if (!dest) {
return;
}
}
if (dest instanceof Array) {
return dest.map(subModel);
} else if (typeof dest === 'object') {
return Object.
keys(dest).
filter(function (key) {
return key[0] !== '$' || key[1] !== '$';
}).
reduce(function (obj, prop) {
obj[prop] = subModel(dest[prop]);
return obj;
}, {});
} else {
return dest;
}
};

@ -0,0 +1,44 @@
module.exports = function summarizeObject (obj) {
var summary = {}, keys;
if (obj instanceof Array) {
keys = obj.map(function (e, i) { return i; });
} else if (typeof obj === 'object') {
keys = Object.keys(obj);
} else {
return '=' + obj.toString().substr(0, 10);
}
var id;
if (keys.some(function (key) {
var lowKey = key.toLowerCase();
if (lowKey.indexOf('name') !== -1 ||
lowKey.indexOf('id') !== -1) {
return id = key;
}
})) {
return '.' + id + '="' + obj[id].toString() + '"';
}
if (keys.length > 5) {
keys = keys.slice(0, 5);
}
keys.forEach(function (key) {
var val = obj[key];
if (val instanceof Array) {
summary[key] = '[ … ]';
} else if (typeof val === 'object') {
summary[key] = '{ … }';
} else if (typeof val === 'function') {
summary[key] = 'fn';
} else {
summary[key] = obj[key].toString();
if (summary[key].length > 10) {
summary[key] = summary[key].substr(0, 10) + '…';
}
}
});
return '=' + JSON.stringify(summary);
};

@ -0,0 +1,46 @@
// throttle based on _.throttle from Lo-Dash
// https://github.com/bestiejs/lodash/blob/master/lodash.js#L4625
// modified so that it
// throttles based on arguments
// returns nothing
// Ex:
// var th = throttle(fn, 50);
// fn('foo'); // not throttled
// fn('foo'); // throttled
// fn('bar'); // not throttled
module.exports = function (func, wait) {
var args,
thisArg,
timeoutId = {},
lastCalled = {};
if (wait === 0) {
return func;
}
return function() {
args = arguments;
thisArg = this;
var argsString = Array.prototype.slice.call(args).join(';');
var now = new Date();
var remaining = wait - (now - lastCalled[argsString]);
if (remaining <= 0) {
clearTimeout(timeoutId[argsString]);
timeoutId[argsString] = null;
lastCalled[argsString] = now;
func.apply(thisArg, args);
}
else if (!timeoutId[argsString]) {
timeoutId[argsString] = setTimeout(function () {
lastCalled[argsString] = new Date();
timeoutId[argsString] = null;
func.apply(thisArg, args);
}, remaining);
}
};
};

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 235 KiB

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

@ -29,7 +29,7 @@ angular.module('panelApp').controller('PerfCtrl', function PerfCtrl($scope, appC
$scope.histogram = histogram;
});
});
appModel.getRootScopes(function (rootScopes) {
appModel.getRootScopeIds(function (rootScopes) {
$scope.$apply(function () {
$scope.roots = rootScopes;
if ($scope.roots.length === 0) {

@ -14,10 +14,10 @@
<link rel="stylesheet" href="css/panel.css">
<!-- libs -->
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/jquery/jquery.js"></script>
<script src="/bower_components/angular/angular.js"></script>
<script src="/bower_components/jquery/jquery.js"></script>
<script src="js/lib/jquery-ui-1.8.21.custom.min.js"></script>
<script src="bower_components/d3/d3.min.js"></script>
<script src="/bower_components/d3/d3.min.js"></script>
<script src="js/panelApp.js"></script>

@ -1,5 +1,5 @@
<html>
<body>
<script src="js/devtoolsBackground.js"></script>
<script src="devtoolsBackground.js"></script>
</body>
</html>

@ -40,6 +40,6 @@ panels.elements.createSidebarPane(
// Angular panel
var angularPanel = panels.create(
"AngularJS",
"img/angular.png",
"panel.html"
"devtools-panel/img/angular.png",
"devtools-panel/panel.html"
);

File diff suppressed because it is too large Load Diff

@ -0,0 +1,61 @@
// Karma configuration for testing injected
// AngularJS instrumentation
module.exports = function (config) {
config.set({
// base path, that will be used to resolve files and exclude
basePath: '',
// frameworks to use
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'test/inject/mock/*.js',
'js/inject/debug.final.js',
'bower_components/angular/angular.js',
'bower_components/angular-mocks/angular-mocks.js',
'test/inject/*.js'
],
// list of files to exclude
exclude: [
'*.min.js'
],
// test results reporter to use
// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['Chrome'],
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false
});
};

@ -1,24 +0,0 @@
files = [
JASMINE,
JASMINE_ADAPTER,
'js/lib/angular.js',
'js/lib/angular-mocks.js',
'js/panelApp.js',
'js/controllers/*.js',
'js/directives/*.js',
'js/filters/*.js',
'js/services/*.js',
'test/mock/*.js',
'test/*.js'
];
exclude = [];
autoWatch = true;
autoWatchInterval = 1;
logLevel = LOG_INFO;
logColors = true;

@ -1,24 +0,0 @@
files = [
JASMINE,
JASMINE_ADAPTER,
'js/lib/angular.js',
'js/lib/angular-mocks.js',
'js/panelApp.js',
'js/controllers/*.js',
'js/directives/*.js',
'js/filters/*.js',
'js/services/*.js',
'test/mock/*.js',
'test/*.js'
];
exclude = [];
autoWatch = true;
autoWatchInterval = 1;
logLevel = LOG_INFO;
logColors = true;

@ -14,7 +14,7 @@
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/inject/debug.js"],
"js": ["content-scripts/inject.build.js"],
"run_at": "document_start"
}
],

@ -0,0 +1,27 @@
// Similar to browserify, but inlines the scripts instead.
// This is a really dumb naive approach that only supports `module.exports =`
var fs = require('fs');
var debug = fs.readFileSync(__dirname + '/../content-scripts/inject.js', 'utf8');
var r = new RegExp("require\\('(.+?)'\\)", 'g');
var out = debug.replace(r, function (match, file) {
return ex(file);
});
fs.writeFileSync(__dirname + '/../content-scripts/inject.build.js', out);
// takes the contents of a file, wraps it in a closure
// and returns the result
function ex (file) {
contents = fs.readFileSync(__dirname + '/../content-scripts/' + file, 'utf8');
contents = contents.replace('module.exports = ', 'return ');
contents = '(function () {\n' +
'// exported from ' + file + '\n' +
contents + '\n' +
'}())';
return contents;
}

@ -0,0 +1,57 @@
describe('inject', function () {
// inject/debug bootstraps asynchronously
beforeEach(function () {});
it('should expose a __ngDebug object to window', function () {
expect(window.__ngDebug).not.toBeUndefined();
});
describe('getRootScopeIds', function () {
it('should start empty', function () {
expect(__ngDebug.getRootScopeIds()).toEqual([]);
});
describe('bootstraped', function () {
it('should work', function () {
var elt, scope;
runs(function () {
angular.module('foo', []).controller('A', function ($scope) {
$scope.model = 1;
$scope.complexModel = { foo: { bar: 'baz' } };
});
elt = angular.element('<div ng-app="foo" ng-controller="A"></div>');
angular.bootstrap(elt, ['ng', 'foo']);
scope = elt.data().$scope;
});
runs(function () {
expect(__ngDebug.getRootScopeIds().length).toBe(1);
expect(__ngDebug.getModel(scope.$id).model).toBe(scope.model);
});
runs(function () {
scope.model = 2;
scope.$digest();
expect(__ngDebug.getModel(scope.$id).model).toBe(2);
});
runs(function () {
__ngDebug.watchModel(scope.$id, 'complexModel');
scope.$digest();
});
waits(60);
runs(function () {
scope.complexModel.b = 1;
scope.$digest();
});
});
});
});
});

@ -0,0 +1,5 @@
// mocks window.chrome.extension
chrome.extension = {
sendMessage: dump
};

@ -0,0 +1,2 @@
// sets the __ngDebug cookie
window.document.cookie = '__ngDebug=true;';