karma
Version:
Spectacular Test Runner for JavaScript.
240 lines (200 loc) • 6.58 kB
JavaScript
var stringify = require('./stringify');
var constant = require('./constants');
var util = require('./util');
/* jshint unused: false */
var Karma = function(socket, iframe, opener, navigator, location) {
var hasError = false;
var startEmitted = false;
var reloadingContext = false;
var store = {};
var self = this;
var queryParams = util.parseQueryParams(location.search);
var browserId = queryParams.id || util.generateId('manual-');
var returnUrl = queryParams.return_url || null;
var currentTransport;
var resultsBufferLimit = 1;
var resultsBuffer = [];
this.VERSION = constant.VERSION;
this.config = {};
var childWindow = null;
var navigateContextTo = function(url) {
if (self.config.useIframe === false) {
if (childWindow === null || childWindow.closed === true) {
// If this is the first time we are opening the window, or the window is closed
childWindow = opener('about:blank');
}
childWindow.location = url;
} else {
iframe.src = url;
}
};
this.setupContext = function(contextWindow) {
if (hasError) {
return;
}
var getConsole = function(currentWindow) {
return currentWindow.console || {
log: function() {},
info: function() {},
warn: function() {},
error: function() {},
debug: function() {}
};
};
contextWindow.__karma__ = this;
// This causes memory leak in Chrome (17.0.963.66)
contextWindow.onerror = function() {
return contextWindow.__karma__.error.apply(contextWindow.__karma__, arguments);
};
contextWindow.onbeforeunload = function(e, b) {
if (!reloadingContext) {
// TODO(vojta): show what test (with explanation about jasmine.UPDATE_INTERVAL)
contextWindow.__karma__.error('Some of your tests did a full page reload!');
}
};
if (self.config.captureConsole) {
// patch the console
var localConsole = contextWindow.console = getConsole(contextWindow);
var browserConsoleLog = localConsole.log;
var logMethods = ['log', 'info', 'warn', 'error', 'debug'];
var patchConsoleMethod = function(method) {
var orig = localConsole[method];
if (!orig) {
return;
}
localConsole[method] = function() {
self.log(method, arguments);
return Function.prototype.apply.call(orig, localConsole, arguments);
};
};
for (var i = 0; i < logMethods.length; i++) {
patchConsoleMethod(logMethods[i]);
}
}
contextWindow.dump = function() {
self.log('dump', arguments);
};
contextWindow.alert = function(msg) {
self.log('alert', [msg]);
};
};
this.log = function(type, args) {
var values = [];
for (var i = 0; i < args.length; i++) {
values.push(this.stringify(args[i], 3));
}
this.info({log: values.join(', '), type: type});
};
this.stringify = stringify;
var clearContext = function() {
reloadingContext = true;
navigateContextTo('about:blank');
};
// error during js file loading (most likely syntax error)
// we are not going to execute at all
this.error = function(msg, url, line) {
hasError = true;
socket.emit('error', url ? msg + '\nat ' + url + (line ? ':' + line : '') : msg);
this.complete();
return false;
};
this.result = function(result) {
if (!startEmitted) {
socket.emit('start', {total: null});
startEmitted = true;
}
if (resultsBufferLimit === 1) {
return socket.emit('result', result);
}
resultsBuffer.push(result);
if (resultsBuffer.length === resultsBufferLimit) {
socket.emit('result', resultsBuffer);
resultsBuffer = [];
}
};
this.complete = function(result) {
if (resultsBuffer.length) {
socket.emit('result', resultsBuffer);
resultsBuffer = [];
}
// give the browser some time to breath, there could be a page reload, but because a bunch of
// tests could run in the same event loop, we wouldn't notice.
setTimeout(function() {
socket.emit('complete', result || {});
clearContext();
// Redirect to the return_url, however we need to give the browser some time,
// so that all the messages are sent.
// TODO(vojta): can we rather get notification from socket.io?
if (returnUrl) {
setTimeout(function() {
location.href = returnUrl;
}, (currentTransport === 'websocket' || currentTransport === 'flashsocket') ? 0 : 3000);
}
}, 0);
};
this.info = function(info) {
// TODO(vojta): introduce special API for this
if (!startEmitted && util.isDefined(info.total)) {
socket.emit('start', info);
startEmitted = true;
} else {
socket.emit('info', info);
}
};
// all files loaded, let's start the execution
this.loaded = function() {
// has error -> cancel
if (!hasError) {
this.start(this.config);
}
// remove reference to child iframe
this.start = null;
};
this.store = function(key, value) {
if (util.isUndefined(value)) {
return store[key];
}
if (util.instanceOf(value, 'Array')) {
var s = store[key] = [];
for (var i = 0; i < value.length; i++) {
s.push(value[i]);
}
} else {
// TODO(vojta): clone objects + deep
store[key] = value;
}
};
// supposed to be overriden by the context
// TODO(vojta): support multiple callbacks (queue)
this.start = function() {
this.error('You need to include some adapter that implements __karma__.start method!');
};
socket.on('execute', function(cfg) {
// reset hasError and reload the iframe
hasError = false;
startEmitted = false;
reloadingContext = false;
self.config = cfg;
navigateContextTo(constant.CONTEXT_URL);
// clear the console before run
// works only on FF (Safari, Chrome do not allow to clear console from js source)
if (window.console && window.console.clear) {
window.console.clear();
}
});
// report browser name, id
socket.on('connect', function() {
currentTransport = socket.socket.transport.name;
// TODO(vojta): make resultsBufferLimit configurable
if (currentTransport === 'websocket' || currentTransport === 'flashsocket') {
resultsBufferLimit = 1;
} else {
resultsBufferLimit = 50;
}
socket.emit('register', {
name: navigator.userAgent,
id: browserId
});
});
};
module.exports = Karma;