UNPKG

testem

Version:

Test'em 'scripts! Javascript Unit testing made easy.

442 lines (397 loc) 12.5 kB
/* testem_client.js ================ The client-side script that reports results back to the Testem server via Socket.IO. It also restarts the tests by refreshing the page when instructed by the server to do so. */ /* globals module */ /* globals jasmineAdapter, jasmine2Adapter, mochaAdapter */ /* globals qunitAdapter, decycle */ /* exported Testem */ 'use strict'; function getTestemIframeSrc() { // Compute a URL to testem/connection.html based on the URL from which this // script was loaded (not the document's base URL, in case the document was // loaded via a file: URL) var scripts = document.getElementsByTagName('script'); var thisScript = scripts[scripts.length - 1]; var a = document.createElement('a'); a.href = thisScript.src; a.pathname = '/testem/connection.html'; return a.href; } function appendTestemIframeOnLoad(callback) { var iframeAppended = false; // Needs to call this synchronously during script load so we know which // <script> tag is loading us and we can grab the right src attribute. var iframeHref = getTestemIframeSrc(); var appendIframe = function() { if (iframeAppended) { return; } iframeAppended = true; var iframe = document.createElement('iframe'); iframe.style.border = 'none'; iframe.style.position = 'fixed'; iframe.style.right = '5px'; iframe.style.bottom = '5px'; iframe.frameBorder = '0'; iframe.allowTransparency = 'true'; iframe.src = iframeHref; document.body.appendChild(iframe); callback(iframe); }; var domReady = function() { if (!document.body) { return setTimeout(domReady, 1); } appendIframe(); }; var DOMContentLoaded = function() { if (document.addEventListener) { document.removeEventListener('DOMContentLoaded', DOMContentLoaded, false); } else { document.detachEvent('onreadystatechange', DOMContentLoaded); } domReady(); }; if (document.addEventListener) { document.addEventListener('DOMContentLoaded', DOMContentLoaded, false); window.addEventListener('load', DOMContentLoaded, false); } else if (document.attachEvent) { document.attachEvent('onreadystatechange', DOMContentLoaded); window.attachEvent('onload', DOMContentLoaded); } if (document.readyState !== 'loading') { domReady(); } } var testFrameworkDidInit = false; function hookIntoTestFramework(socket) { if (testFrameworkDidInit) { return; } var found = true; if (typeof getJasmineRequireObj === 'function') { jasmine2Adapter(socket); } else if (typeof jasmine === 'object') { jasmineAdapter(socket); } else if (typeof Mocha === 'function') { mochaAdapter(socket); } else if (typeof QUnit === 'object') { qunitAdapter(socket); } else { found = false; } testFrameworkDidInit = found; return found; } var addListener; if (typeof window !== 'undefined') { addListener = window.addEventListener ? function(obj, evt, cb) { obj.addEventListener(evt, cb, false); } : function(obj, evt, cb) { obj.attachEvent('on' + evt, cb); }; } // Used internally in order to remember state involving a message that needs to // be fired after a delay. It matters which socket sends the message, because // the socket is configurable by custom adapters. function Message(socket, emitArgs) { this.socket = socket; this.emitArgs = emitArgs; } // eslint-disable-next-line no-use-before-define if (typeof TestemConfig === 'undefined') { var TestemConfig = {}; } var Testem = { emitMessageQueue: [], afterTestsQueue: [], console: {}, // The maximum depth beyond which decycle will truncate an emitted event // object. When undefined, decycle uses its default. decycleDepth: TestemConfig.decycle_depth, useCustomAdapter: function(adapter) { adapter(new TestemSocket()); }, getId: function() { // If the test page defined a custom method for discovering our id, use // that if (window.getTestemId) { return window.getTestemId(); } var match = window.location.pathname.match(/^\/(-?[0-9]+)/); return match ? match[1] : null; }, emitMessage: function() { if (this._noConnectionRequired) { return; } var args = new Array(arguments.length); for (var i = 0; i < args.length; ++i) { args[i] = arguments[i]; } var message = new Message(this, args); if (this._isIframeReady) { this.emitMessageToIframe(message); } else { // enqueue until iframe is ready this.enqueueMessage(message); } }, emit: function(evt) { var argsWithoutFirst = new Array(arguments.length - 1); for (var i = 1; i < arguments.length; ++i) { argsWithoutFirst[i - 1] = arguments[i]; } if (this.evtHandlers && this.evtHandlers[evt]) { var handlers = this.evtHandlers[evt]; for (var j = 0; j < handlers.length; j++) { var handler = handlers[j]; handler.apply(this, argsWithoutFirst); } } this.emitMessage.apply(this, arguments); }, on: function(evt, callback) { if (!this.evtHandlers) { this.evtHandlers = {}; } if (!this.evtHandlers[evt]) { this.evtHandlers[evt] = []; } this.evtHandlers[evt].push(callback); }, handleConsoleMessage: null, noConnectionRequired: function() { this._noConnectionRequired = true; this.emitMessageQueue = []; }, emitMessageToIframe: function(message) { message.socket.sendMessageToIframe('emit-message', message.emitArgs); }, sendMessageToIframe: function(type, data) { var message = { type: type }; var decycleDepth = -1; if (data) { message.data = data; if (data[0] === 'browser-console') { // User content in data decycleDepth = this.decycleDepth + 1; } else if (data[0] === 'test-result') { // User content in data.test.items decycleDepth = this.decycleDepth + 3; } else if (data[0] === 'all-test-results') { // User content in data.tests.test.items decycleDepth = this.decycleDepth + 4; } else { // Events don't contain user content / cycles decycleDepth = -1; } } message = this.serializeMessage(message, decycleDepth); this.iframe.contentWindow.postMessage(message, '*'); }, enqueueMessage: function(message) { if (this._noConnectionRequired) { return; } this.emitMessageQueue.push(message); }, iframeReady: function() { this.drainMessageQueue(); this._isIframeReady = true; }, drainMessageQueue: function() { while (this.emitMessageQueue.length) { var item = this.emitMessageQueue.shift(); this.emitMessageToIframe(item); } }, listenTo: function(iframe) { this.iframe = iframe; var self = this; addListener(window, 'message', function messageListener(event) { if (event.source !== self.iframe.contentWindow) { // ignore messages not from the iframe return; } var message = self.deserializeMessage(event.data); var type = message.type; switch (type) { case 'reload': self.reload(); break; case 'get-id': self.sendId(); break; case 'no-connection-required': self.noConnectionRequired(); break; case 'iframe-ready': self.iframeReady(); break; case 'tap-all-test-results': self.emit('tap-all-test-results'); break; case 'stop-run': self.emit('after-tests-complete'); break; default: if (type && type.indexOf('testem:') === 0) { self.emit(type, message.data); } break; } }); }, sendId: function() { this.sendMessageToIframe('get-id', this.getId()); }, reload: function() { window.location.reload(); }, deserializeMessage: function(message) { return JSON.parse(message); }, serializeMessage: function(message, depth) { // decycle to remove possible cyclic references if (depth !== -1) { message = decycle(message, depth); } // stringify for clients that only can handle string postMessages (IE <= 10) return JSON.stringify(message); }, removeEventCallbacks: function(evt, callback) { var handlers = this.evtHandlers[evt]; var removeIdx = []; if (typeof handlers === 'undefined') { return; } for (var i = 0; i < handlers.length; i++) { if (handlers[i] === callback) { removeIdx.push(i); } } for (var j = 0; j < removeIdx.length; j++) { handlers.splice(j, 1); } }, runAfterTests: function() { if (Testem.afterTestsQueue.length) { var afterTestsCallback = Testem.afterTestsQueue.shift(); if (typeof afterTestsCallback !== 'function') { throw Error('Callback not a function'); } else { afterTestsCallback.call(this, null, null, Testem.runAfterTests); } } else { emit('after-tests-complete'); } }, afterTests: function(cb) { Testem.afterTestsQueue.push(cb); } }; // Represents a configurable socket on top of window.Testem, which is provided // to each custom adapter. function TestemSocket() {} TestemSocket.prototype = Testem; // Exporting this as a module so that it can be unit tested in Node. if (typeof module !== 'undefined') { module.exports = Testem; } function init() { appendTestemIframeOnLoad(function(iframe) { Testem.listenTo(iframe); }); interceptWindowOnError(); takeOverConsole(); setupTestStats(); Testem.hookIntoTestFramework = function() { if (!hookIntoTestFramework(Testem)) { throw new Error('Testem was unable to detect a test framework, please load it before invoking Testem.hookIntoTestFramework'); } }; hookIntoTestFramework(Testem); Testem.on('all-test-results', Testem.runAfterTests); Testem.on('tap-all-test-results', Testem.runAfterTests); } function setupTestStats() { var originalTitle = document.title; var total = 0; var passed = 0; Testem.on('test-result', function(test) { total++; if (test.failed === 0) { passed++; } updateTitle(); }); function updateTitle() { if (!total) { return; } document.title = originalTitle + ' (' + passed + '/' + total + ')'; } } function takeOverConsole() { function intercept(method) { var original = console[method]; Testem.console[method] = original; console[method] = function() { var doDefault, message; var args = new Array(arguments.length); for (var i = 0; i < args.length; ++i) { args[i] = arguments[i]; } if (Testem.handleConsoleMessage) { message = decycle(args, Testem.decycleDepth).join(' '); doDefault = Testem.handleConsoleMessage(message); } if (doDefault !== false) { args.unshift(method); args.unshift('browser-console'); emit.apply(Testem, args); if (typeof original === 'object') { // Do this for IE Function.prototype.apply.call(original, console, arguments); } else { // Do this for normal browsers original.apply(console, arguments); } } }; } var methods = ['log', 'warn', 'error', 'info', 'group']; for (var i = 0; i < methods.length; i++) { if (window.console && console[methods[i]]) { intercept(methods[i]); } } } function interceptWindowOnError() { var orginalOnError = window.onerror; window.onerror = function(msg, url, line) { if (typeof msg === 'string' && typeof url === 'string' && typeof line === 'number') { emit('top-level-error', msg, url, line); } if (orginalOnError) { orginalOnError.apply(window, arguments); } }; } function emit() { Testem.emit.apply(Testem, arguments); } if (typeof window !== 'undefined') { window.Testem = Testem; // Stub window.alert and window.confirm to throw error so alert and confirm does not get used in tests // this will prevent browser disconnect failures window.alert = function() { throw new Error('[Testem] Calling window.alert() in tests is disabled, because it causes testem to fail with browser disconnect error.'); }; window.confirm = function() { throw new Error('[Testem] Calling window.confirm() in tests is disabled, because it causes testem to fail with browser disconnect error.'); }; init(); }