Last active
December 16, 2015 08:18
-
-
Save Satyam/5404590 to your computer and use it in GitHub Desktop.
This is related to this conversation: https://groups.google.com/forum/?hl=en&fromgroups=#!topic/yui-contrib/hP-Qg2jLQXo. It logs DOM Nodes, cached Node instances and DOM events left behind after a test case in YUI Test. It takes a snapshot of short handles that can help identify them in the setUp() function of each test case and compares them wi…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// How to use it. | |
// | |
// This code is based on the template file that YOGI produces for unit testing | |
// when a module is created. Only the main changes to it are listed. | |
// | |
// Add a reference to the module in the YUI configuration section so it can locate it | |
// It's up to you where you put it. | |
YUI({ | |
groups: { | |
leaks: { | |
base: 'assets/', | |
modules: { | |
'leak-utils': { | |
path: 'leakutils.js', | |
requires: [ | |
'test', 'node-base' | |
] | |
} | |
} | |
} | |
} | |
// Loading needs to be done in two stages. | |
// In the first stage you have to load this utility along the `test` and | |
// `base-core` modules so the utility can patch those two. | |
// The `base-core` needs to be loaded before any other module that depends | |
// on it, otherwise, the classes in those modules will not inherit the patched-up | |
// version but the original unpatched one. | |
}).use( | |
'base-core', | |
'test', | |
'leak-utils', | |
// Now you enable the leak detector so it patches `test` and `base-core` | |
function (Y) { | |
// I made it conditional on a URL argument | |
if (/[?&]leak=([^&]+)/.test(window.location.search)) { | |
Y.Test.Runner.setDOMIgnore('#logger', 'script'); | |
Y.Test.Runner.enableLeakDetector(); | |
} | |
// Then you get to load the rest as you would normally do. | |
Y.use( | |
'test-console', | |
// ...... Whatever modules you are testing ... | |
function (Y) { | |
// From here on, there regular stuff produced by yogi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
The previous file shows how to integrate this into your module tests. | |
*/ | |
YUI.add('leak-utils', function(Y) { | |
var DOMNodes, // Stores a snapshot of the existing DOM Nodes | |
DOMEvents, // Stores a snapshot of DOM events | |
baseInstances, // Array of base instances created | |
collectBase = false, // signals whether to collect base instances or not. | |
inCase = false, // says whether I'm in a test case or not | |
arrEach = Y.Array.each, | |
objEach = Y.Object.each, | |
// Stores the info to show after each test case ends | |
logs = {}, | |
// Shows the logs | |
showLogs = function (name) { | |
Y.log(name,'leak','TestRunner'); | |
objEach(logs, function (section, key) { | |
if (section.length) { | |
Y.log(' ' + key,'leak','TestRunner'); | |
arrEach(section, function (msg) { | |
Y.log(' ' + msg,'leak','TestRunner'); | |
}); | |
} | |
}); | |
logs = {}; | |
}, | |
// Produces as CSS-selector type of signature for a node or HTML element | |
signature = function (n) { | |
var tag, id, cname; | |
if (n.get) { | |
tag = n.get('tagName') || n.get('nodeName'); | |
id = n.get('id'); | |
cname = n.get('className'); | |
} else { | |
tag = n.tagName || n.nodeName; | |
id = n.id; | |
cname = n.className; | |
} | |
if (!tag) { | |
return tag; | |
} | |
switch (tag.toUpperCase()) { | |
case 'HTML': | |
case 'BODY': | |
return; | |
} | |
return tag + (id ? '#' + id : '') + ( cname ? '.' + cname.replace(' ','.') : ''); | |
}, | |
// Takes a snapshot of what's in the document body | |
snapShotDOM = function () { | |
var excludes = Y.Test.Runner._DOMIgnore, | |
addNode = function (n) { | |
if (Y.some(excludes, function (x) { | |
return n.test(x); | |
})) { | |
return null; | |
} | |
var item = signature(n); | |
if (item) { | |
DOMNodes.push(item); | |
} | |
n.get('children').each(addNode); | |
}; | |
DOMNodes = []; | |
addNode(Y.one('body')); | |
}, | |
// Compares the document body with a previous snapshot | |
cmpDOM = function () { | |
var excludes = Y.Test.Runner._DOMIgnore, | |
leftovers = [], | |
missing = [], | |
cmpNode = function (n) { | |
if (Y.some(excludes, function (x) { | |
return n.test(x); | |
})) { | |
return false; | |
} | |
var item = signature(n), i; | |
if (item) { | |
i = DOMNodes.indexOf(item); | |
if (i < 0) { | |
leftovers.push(item); | |
} else { | |
delete DOMNodes[i]; | |
} | |
} | |
n.get('children').each(cmpNode); | |
}; | |
cmpNode(Y.one('body')); | |
arrEach(DOMNodes, function (item) { | |
if (item) { | |
missing.push(item); | |
} | |
}); | |
logs['Leftover DOM nodes'] = leftovers; | |
logs['Missing DOM nodes'] = missing; | |
DOMNodes = null; | |
}, | |
// Instead of taking a snapshot of the cache I found it easier | |
// to simply whipe it out and count from there | |
snapShotNodes = function () { | |
Y.Node._instances = {}; | |
}, | |
// Lists cached Node references | |
cmpNodes = function () { | |
var excludes = Y.all(Y.Test.Runner._DOMIgnore.join(',')), | |
leftovers = [], | |
extrasindoc = []; | |
objEach(Y.Node._instances, function (n) { | |
if (excludes.some(function (x) { | |
return x.contains(n); | |
})) { | |
return; | |
} | |
var indoc = false, | |
item = signature(n); | |
if (item) { | |
try { | |
indoc = n.inDoc(); | |
} | |
catch (e) {} | |
if (indoc) { | |
extrasindoc.push(item); | |
} else { | |
leftovers.push(item); | |
} | |
} | |
}); | |
logs['Leftover cached Nodes'] = leftovers; | |
logs['Leftover cached Nodes still in doc'] = extrasindoc; | |
}, | |
// Takes a snapshot of DOM events | |
snapShotDOMEvents = function () { | |
var excludes = Y.all(Y.Test.Runner._DOMIgnore.join(',')); | |
DOMEvents = []; | |
objEach(Y.Env.evt.dom_map, function (item) { | |
objEach(item, function (ev, key) { | |
if (excludes.some(function (x) { | |
return x.contains(ev.el); | |
})) { | |
return; | |
} | |
DOMEvents.push(key); | |
}); | |
}); | |
}, | |
// Checks for DOM Events left behind | |
cmpDOMEvents = function () { | |
var excludes = Y.all(Y.Test.Runner._DOMIgnore.join(',')), | |
leftovers = []; | |
objEach(Y.Env.evt.dom_map, function (item) { | |
objEach(item, function (ev, key) { | |
if (0 < DOMEvents.indexOf(key)) { | |
if (excludes.some(function (x) { | |
return x.contains(ev.el); | |
})) { | |
return; | |
} | |
if (ev.type === '_synth') { | |
arrEach(ev.handles, function (item) { | |
leftovers.push(item.evt.type + ': ' + signature(ev.el)); | |
}); | |
} else { | |
leftovers.push(ev.type + ': ' + signature(ev.el)); | |
} | |
} | |
}); | |
}); | |
logs['Leftover DOM Events'] = leftovers; | |
DOMEvents = null; | |
}, | |
snapShotBase = function () { | |
baseInstances = {}; | |
collectBase = true; | |
}, | |
cmpBase = function () { | |
collectBase = false; | |
var leftOvers = []; | |
objEach(baseInstances, function (name, yuid) { | |
if (name) { | |
leftOvers.push(name + '#' + yuid); | |
} | |
}); | |
logs['Leftover Base instances'] = leftOvers; | |
baseInstances = null; | |
}; | |
// Creates the list of elements to be ignored. | |
Y.Test.Runner.setDOMIgnore = function () { | |
this._DOMIgnore = Y.Array(arguments); | |
}; | |
// Enables the leak detector | |
Y.Test.Runner.enableLeakDetector = function () { | |
Y.BaseCore.prototype._initBase = (function (original) { | |
return function () { | |
var ret = original.apply(this, arguments); | |
if (collectBase) { | |
baseInstances[this._yuid] = this.name; | |
} | |
return ret; | |
}; | |
})(Y.BaseCore.prototype._initBase); | |
Y.BaseCore.prototype._baseDestroy = (function (original) { | |
return function () { | |
if (collectBase) { | |
baseInstances[this._yuid] = null; | |
} | |
return original.apply(this, arguments); | |
}; | |
})(Y.BaseCore.prototype._baseDestroy); | |
// Monkey patches a native test runner method. | |
Y.Test.Runner._execNonTestMethod = (function (original) { | |
return function (node, methodName) { | |
var ret; | |
switch (methodName) { | |
case 'setUp': | |
if (inCase) { | |
snapShotDOM(); | |
snapShotNodes(); | |
snapShotDOMEvents(); | |
snapShotBase(); | |
} | |
ret = original.apply(this, arguments); | |
break; | |
case 'tearDown': | |
ret = original.apply(this, arguments); | |
if (inCase) { | |
cmpDOM(); | |
cmpNodes(); | |
cmpDOMEvents(); | |
cmpBase(); | |
if (typeof this._cur.testObject === 'string') { | |
showLogs(node.testObject.name + '\n ' + this._cur.testObject + '\n'); | |
} | |
} | |
break; | |
case 'init': | |
inCase = true; | |
ret = original.apply(this, arguments); | |
break; | |
case 'destroy': | |
inCase = false; | |
ret = original.apply(this, arguments); | |
break; | |
default: | |
ret = original.apply(this, arguments); | |
} | |
return ret; | |
}; | |
})(Y.Test.Runner._execNonTestMethod); | |
}; | |
},'', {requires: [ 'test', 'node-base']}); |
\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/\o/
Many thanks. I must admit I didn't do unit testing yet. Still is part of my learning-curve. Now I have to use it ;)
Hi Satyam,
It took me a while before I could use your leak-test. Quite busy here, and had to find out how to work with untitesting.
I am using it now and it helped me get rid of some leaks I really needed to get rid of. So thx!
However, I had some small (solved) issues that I wanted to share:
- I suffered that cmpBase was running before some of the actual base-instances were destroyed. In the tearDown, I called myModel.destroy(), but destruction of base-instances runs through the eventsystem, therefore later -in my case at least- than cmpBase ran. I fixed this by making calling cmpBase asynchronous, changing line 252 into:
Y.soon(cmpBase);
- For some reason, logging didn't work here. I created the module and load it through our own comboloader. I couldn't find out for now the reason why, but I used this monkey-fix to make it work:
showLogs = function (name) {
var firtsentry = true;
objEach(logs, function (section, key) {
if (section.length) {
if (firtsentry) {
this.log(name,'leak','TestRunner');
firtsentry = false;
}
this.log(' ' + key,'leak','TestRunner');
arrEach(section, function (msg) {
this.log(' ' + msg,'leak','TestRunner');
});
}
});
logs = {};
},
It looks like a problem with the context it is running. Because I got the loggin working right now, I'm not going to search deeper for the reason. Perhaps at a later time.
Thanks for creating this awesome feature, which should be part of the unittests by default.
Regards,
Marco.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great stuff!
Instead of requiring the
node
alias, you should probably requirenode-base
(as this module has no need ofnode-event-delegate
,node-pluginhost
,node-screen
, ornode-style
). This helps avoid extra baggage when the module being tested has limited dependencies.