Skip to content

Instantly share code, notes, and snippets.

@Satyam
Last active December 16, 2015 08:18
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Satyam/5404590 to your computer and use it in GitHub Desktop.
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…
// 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
/*
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']});
@evocateur
Copy link

Great stuff!

Instead of requiring the node alias, you should probably require node-base (as this module has no need of node-event-delegate, node-pluginhost, node-screen, or node-style). This helps avoid extra baggage when the module being tested has limited dependencies.

@ItsAsbreuk
Copy link

\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 ;)

@ItsAsbreuk
Copy link

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:

  1. 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);
  1. 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