Skip to content

Instantly share code, notes, and snippets.

@iki
Last active August 29, 2015 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iki/9371373 to your computer and use it in GitHub Desktop.
Save iki/9371373 to your computer and use it in GitHub Desktop.
Enable logs from your unit tests using logging.on|off() methods
/*Enable logs from your unit tests using logging.on|off() methods.
Provides stringifier to stripped JSON for safe logging of objects
with cyclical references, DOM elements, angular scopes, or window.
By default, logging.on/off() works with window.console,
or with $log service if angular is detected.
By default, logging class attaches to jasmine.logging,
or mocha.logging, or window.logging.
By default logging uses karma test runner log,
or expect(logMessage).toEqual(args),
or expect(logMessage).to.eql(args).
Using expect() to output logs fails the tests, though.
Source+comments: https://gist.github.com/iki/9371373
*/
'use strict';
(function(options) {
var L = {
name: 'logging',
maxDepth: 4, // TODO: implement when needed.
filterDOMElements: true,
filterAngularProperties: true,
methods: {'log': 'info', 'info': 'info', 'warn': 'warn', 'error': 'error', 'debug': 'debug'},
stringify: function(data, options) {
/* Stringifier to stripped JSON for safe logging of objects
with cyclical references, DOM elements, angular scopes, or window.
Prevents `TypeError: Converting circular structure to JSON`
by replacing circular references with '<ref:KEY>'.
Prevents `RangeError: Maximum call stack size exceeded`.
However, it's recommended to use `maxDepth` or `filterObjects` anyway,
because serializing very deep objects costs both time and space,
which may lower its usability for general logging,
and even make the test browser disconnect when used in tests.
Optionally:
* limits object inspection depth (not implemented yet),
* filters specified objects (like window, test framework, test runner),
* filters angular object $attributes,
* replaces angular scopes in $attributes with '<scope:$ID_PATH>',
* replaces DOM elements with '<dom:ELEMENT_PATH>'.
*/
var seen = [], keys = [], self = L;
if (!options) {
options = self;
}
var pass = function(arg) {
return arg;
};
return JSON.stringify(data, function(key, value) {
var index;
if (typeof value === 'object') {
if (options.filterAngularProperties && key && key[0] === '$') {
return value && value.$id ? '<scope:' + self.stringifyNgScopeId(value) + '>' : undefined;
}
if (value) {
if (options.filterDOMElements && 'firstElementChild' in value) {
return '<dom:' + (self.stringifyDOMElementPath(value) || '-') + '>' +
// (value.innerHTML ? "\n--- inner html: " + value.innerHTML : '') +
// (value.outerHTML ? "\n--- outer html: " + value.outerHTML : '') +
'';
}
if ((index = options.filterObjectIndex.indexOf(value)) !== -1) {
return '<' + options.filterObjectNames[index] + '>';
}
try {
index = seen.indexOf(pass(value));
} catch(error) { // Call stack size exceeded.
return '<' + error.toString().toLowerCase() + '>';
}
if (index !== -1) {
var topkey = keys[index];
var path = [topkey];
for (index--; index > 0; index--) {
if (seen[index][topkey] === value) {
value = seen[index];
path.unshift(topkey = keys[index]);
}
}
return '<ref:' + path.join('.') + '>';
}
seen.push(value);
keys.push(key);
}
}
return value;
}, 2);
},
stringifyNgScopeId: function(s) {
for (var id = []; s && s.$id; s = s.$parent) {
id.unshift(s.$id);
}
return id.join('-').toLowerCase();
},
stringifyDOMElementPath: function(e) {
for (var id = []; e && e.tagName; e = e.parentElement) {
id.unshift(e.tagName);
}
return id.join('.').toLowerCase();
},
timeStamp: function(label) {
if (L.logger.timeStamp) {
L.logger.timeStamp(label);
} else if (L.console.timeStamp) {
L.console.timeStamp(label);
}
Function.prototype.apply.call(L.logger.info, L.logger, arguments);
},
on: function(logger) {
if (!logger) {
return L.__on();
}
var setup = function(method, level) {
level = level || method;
var orig = logger[method];
var header = '[' + level.toUpperCase() + '] ';
logger[method] = function(msg) {
var args = Array.prototype.slice.call(arguments, 1);
if (L.testRunner) {
return L.testRunner.info({log: msg + '\n' + L.stringify(args), type: level});
} else if (L.window.expect) {
var expected = expect(header + msg);
if (expected.toEqual) {
return expected.toEqual(args);
} else if (expected.to && expected.to.eql) {
return expected.to.eql(args);
}
}
return orig ? Function.prototype.apply.call(orig, logger, arguments) : false;
};
for (var key in orig) {
logger[method][key] = orig[key];
}
logger[method].__orig__ = orig;
};
for (var name in L.methods) {
setup(name, L.methods[name]);
}
},
off: function(logger) {
if (!logger) {
return L.__off();
}
var setup = function(method, level) {
level = level || method;
if (logger[method] && logger[method].__orig__) {
logger[method] = logger[method].__orig__;
}
};
for (var name in L.methods) {
setup(name, L.methods[name]);
}
},
configure: function(options) {
var O = options || {};
var W = L.window = O.window || L.window || window;
var C = L.console = L.window.console || {};
L.name = O.name || L.name;
L.logger = O.logger || C;
L.ngName = O.ngName || '$log';
L.ngModules = O.ngModules || [];
L.maxDepth = 'maxDepth' in O ? O.maxDepth : L.maxDepth;
L.filterDOMElements = 'filterDOMElements' in O ? O.filterDOMElements : L.filterDOMElements;
L.filterAngularProperties = 'filterAngularProperties' in O ? O.filterAngularProperties : L.filterAngularProperties;
L.testFramework = O.testFramework || W.jasmine || W.mocha;
L.attachTo = O.attachTo || L.testFramework || W;
L.testRunner = 'testRunner' in O ? (O.testRunner.info ? O.testRunner : false) :
W.__karma__ || false;
L.filterObjects = O.filterObjects || L.filterObjects ||
{window: W, console: C, karma: W.__karma__, jasmine: W.jasmine, mocha: W.mocha,
x_logging_parent: L.attachTo, x_test_framework: L.testFramework, x_test_runner: L.testRunner};
L.filterObjectIndex = [];
L.filterObjectNames = [];
for (var key in L.filterObjects) {
L.filterObjectNames.push(key);
L.filterObjectIndex.push(L.filterObjects[key]);
}
if (L.testFramework) {
if (W.angular) {
if (L.ngName) {
if (W.inject) {
L.__on = W.inject([L.ngName, L.on]);
L.__off = W.inject([L.ngName, L.off]);
} else {
W.console.warn('logging: missing supported api to inject angular dependency: ' + L.ngName);
}
}
if (L.ngModules && L.ngModules.length) {
if (W.beforeEach && W.module) {
W.beforeEach(function() {
for (var _i = L.ngModules.length; _i--;) {
W.module(L.ngModules[_i]);
}
});
} else {
W.console.warn('logging: missing any supported api to register angular modules', L.ngModules);
}
}
}
}
if (!L.__on && L.logger) {
L.__on = function() {
L.on(L.logger);
};
L.__off = function() {
L.off(L.logger);
};
}
L.attachTo[L.name] = L;
if (O.debug) {
W.console.info('logging: configured: ' + L.stringify(L));
}
return L;
}
};
return L.configure(options);
})({window: window});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment