diff --git a/js/tests/vendor/qunit.css b/js/tests/vendor/qunit.css
index 75d8b6279f677548134364f7aa27a3a58817d0ae..7a469353341b4a610fe6ec41ae40b5461854305f 100644
--- a/js/tests/vendor/qunit.css
+++ b/js/tests/vendor/qunit.css
@@ -1,12 +1,12 @@
 /*!
- * QUnit 2.3.2
+ * QUnit 2.3.3
  * https://qunitjs.com/
  *
  * Copyright jQuery Foundation and other contributors
  * Released under the MIT license
  * https://jquery.org/license
  *
- * Date: 2017-04-18T02:19Z
+ * Date: 2017-06-02T14:07Z
  */
 
 /** Font Family and Sizes */
diff --git a/js/tests/vendor/qunit.js b/js/tests/vendor/qunit.js
index fa728db5b61e421e05acf4e0e62989fc8ae534e2..3cda99631a9542dd813c6a0a22f61bcfccef4f52 100644
--- a/js/tests/vendor/qunit.js
+++ b/js/tests/vendor/qunit.js
@@ -1,12 +1,12 @@
 /*!
- * QUnit 2.3.2
+ * QUnit 2.3.3
  * https://qunitjs.com/
  *
  * Copyright jQuery Foundation and other contributors
  * Released under the MIT license
  * https://jquery.org/license
  *
- * Date: 2017-04-18T02:19Z
+ * Date: 2017-06-02T14:07Z
  */
 (function (global$1) {
   'use strict';
@@ -14,6 +14,7 @@
   global$1 = 'default' in global$1 ? global$1['default'] : global$1;
 
   var window = global$1.window;
+  var self$1 = global$1.self;
   var console = global$1.console;
   var setTimeout = global$1.setTimeout;
   var clearTimeout = global$1.clearTimeout;
@@ -238,6 +239,27 @@
   	return objectType(obj) === type;
   }
 
+  // Based on Java's String.hashCode, a simple but not
+  // rigorously collision resistant hashing function
+  function generateHash(module, testName) {
+  	var str = module + "\x1C" + testName;
+  	var hash = 0;
+
+  	for (var i = 0; i < str.length; i++) {
+  		hash = (hash << 5) - hash + str.charCodeAt(i);
+  		hash |= 0;
+  	}
+
+  	// Convert the possibly negative integer hash code into an 8 character hex string, which isn't
+  	// strictly necessary but increases user understanding that the id is a SHA-like hash
+  	var hex = (0x100000000 + hash).toString(16);
+  	if (hex.length < 8) {
+  		hex = "0000000" + hex;
+  	}
+
+  	return hex.slice(-8);
+  }
+
   // Test for equality any JavaScript type.
   // Authors: Philippe Rathé <prathe@gmail.com>, David Chan <david@troi.org>
   var equiv = (function () {
@@ -608,21 +630,19 @@
   	// Set of all modules.
   	modules: [],
 
-  	// Stack of nested modules
-  	moduleStack: [],
-
   	// The first unnamed module
   	currentModule: {
   		name: "",
   		tests: [],
   		childModules: [],
-  		testsRun: 0
+  		testsRun: 0,
+  		unskippedTestsRun: 0
   	},
 
   	callbacks: {},
 
   	// The storage module to use for reordering tests
-  	storage: sessionStorage
+  	storage: localSessionStorage
   };
 
   // take a predefined QUnit.config and extend the defaults
@@ -1064,6 +1084,135 @@
   	return extractStacktrace(error, offset);
   }
 
+  var priorityCount = 0;
+  var unitSampler = void 0;
+
+  /**
+   * Advances the ProcessingQueue to the next item if it is ready.
+   * @param {Boolean} last
+   */
+  function advance() {
+  	var start = now();
+  	config.depth = (config.depth || 0) + 1;
+
+  	while (config.queue.length && !config.blocking) {
+  		var elapsedTime = now() - start;
+
+  		if (!defined.setTimeout || config.updateRate <= 0 || elapsedTime < config.updateRate) {
+  			if (priorityCount > 0) {
+  				priorityCount--;
+  			}
+
+  			config.queue.shift()();
+  		} else {
+  			setTimeout(advance, 13);
+  			break;
+  		}
+  	}
+
+  	config.depth--;
+
+  	if (!config.blocking && !config.queue.length && config.depth === 0) {
+  		done();
+  	}
+  }
+
+  function addToQueueImmediate(callback) {
+  	if (objectType(callback) === "array") {
+  		while (callback.length) {
+  			addToQueueImmediate(callback.pop());
+  		}
+
+  		return;
+  	}
+
+  	config.queue.unshift(callback);
+  	priorityCount++;
+  }
+
+  /**
+   * Adds a function to the ProcessingQueue for execution.
+   * @param {Function|Array} callback
+   * @param {Boolean} priority
+   * @param {String} seed
+   */
+  function addToQueue(callback, prioritize, seed) {
+  	if (prioritize) {
+  		config.queue.splice(priorityCount++, 0, callback);
+  	} else if (seed) {
+  		if (!unitSampler) {
+  			unitSampler = unitSamplerGenerator(seed);
+  		}
+
+  		// Insert into a random position after all prioritized items
+  		var index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1));
+  		config.queue.splice(priorityCount + index, 0, callback);
+  	} else {
+  		config.queue.push(callback);
+  	}
+  }
+
+  /**
+   * Creates a seeded "sample" generator which is used for randomizing tests.
+   */
+  function unitSamplerGenerator(seed) {
+
+  	// 32-bit xorshift, requires only a nonzero seed
+  	// http://excamera.com/sphinx/article-xorshift.html
+  	var sample = parseInt(generateHash(seed), 16) || -1;
+  	return function () {
+  		sample ^= sample << 13;
+  		sample ^= sample >>> 17;
+  		sample ^= sample << 5;
+
+  		// ECMAScript has no unsigned number type
+  		if (sample < 0) {
+  			sample += 0x100000000;
+  		}
+
+  		return sample / 0x100000000;
+  	};
+  }
+
+  /**
+   * This function is called when the ProcessingQueue is done processing all
+   * items. It handles emitting the final run events.
+   */
+  function done() {
+  	var storage = config.storage;
+
+  	ProcessingQueue.finished = true;
+
+  	var runtime = now() - config.started;
+  	var passed = config.stats.all - config.stats.bad;
+
+  	emit("runEnd", globalSuite.end(true));
+  	runLoggingCallbacks("done", {
+  		passed: passed,
+  		failed: config.stats.bad,
+  		total: config.stats.all,
+  		runtime: runtime
+  	});
+
+  	// Clear own storage items if all tests passed
+  	if (storage && config.stats.bad === 0) {
+  		for (var i = storage.length - 1; i >= 0; i--) {
+  			var key = storage.key(i);
+
+  			if (key.indexOf("qunit-test-") === 0) {
+  				storage.removeItem(key);
+  			}
+  		}
+  	}
+  }
+
+  var ProcessingQueue = {
+  	finished: false,
+  	add: addToQueue,
+  	addImmediate: addToQueueImmediate,
+  	advance: advance
+  };
+
   var TestReport = function () {
   	function TestReport(name, suite, options) {
   		classCallCheck(this, TestReport);
@@ -1077,6 +1226,8 @@
   		this.skipped = !!options.skip;
   		this.todo = !!options.todo;
 
+  		this.valid = options.valid;
+
   		this._startTime = 0;
   		this._endTime = 0;
 
@@ -1149,13 +1300,24 @@
   		value: function getAssertions() {
   			return this.assertions.slice();
   		}
+
+  		// Remove actual and expected values from assertions. This is to prevent
+  		// leaking memory throughout a test suite.
+
+  	}, {
+  		key: "slimAssertions",
+  		value: function slimAssertions() {
+  			this.assertions = this.assertions.map(function (assertion) {
+  				delete assertion.actual;
+  				delete assertion.expected;
+  				return assertion;
+  			});
+  		}
   	}]);
   	return TestReport;
   }();
 
-  var unitSampler;
   var focused = false;
-  var priorityCount = 0;
 
   function Test(settings) {
   	var i, l;
@@ -1166,14 +1328,14 @@
   	extend(this, settings);
   	this.assertions = [];
   	this.semaphore = 0;
-  	this.usedAsync = false;
   	this.module = config.currentModule;
   	this.stack = sourceFromStacktrace(3);
   	this.steps = [];
 
   	this.testReport = new TestReport(settings.testName, this.module.suiteReport, {
   		todo: settings.todo,
-  		skip: settings.skip
+  		skip: settings.skip,
+  		valid: this.valid()
   	});
 
   	// Register unique strings
@@ -1187,7 +1349,8 @@
 
   	this.module.tests.push({
   		name: this.testName,
-  		testId: this.testId
+  		testId: this.testId,
+  		skip: !!settings.skip
   	});
 
   	if (settings.skip) {
@@ -1234,12 +1397,6 @@
 
   		config.current = this;
 
-  		if (module.testEnvironment) {
-  			delete module.testEnvironment.before;
-  			delete module.testEnvironment.beforeEach;
-  			delete module.testEnvironment.afterEach;
-  			delete module.testEnvironment.after;
-  		}
   		this.testEnvironment = extend({}, module.testEnvironment);
 
   		this.started = now();
@@ -1297,14 +1454,14 @@
   		    test = this;
   		return function runHook() {
   			if (hookName === "before") {
-  				if (hookOwner.testsRun !== 0) {
+  				if (hookOwner.unskippedTestsRun !== 0) {
   					return;
   				}
 
   				test.preserveEnvironment = true;
   			}
 
-  			if (hookName === "after" && hookOwner.testsRun !== numberOfTests(hookOwner) - 1) {
+  			if (hookName === "after" && hookOwner.unskippedTestsRun !== numberOfUnskippedTests(hookOwner) - 1 && config.queue.length > 2) {
   				return;
   			}
 
@@ -1334,8 +1491,8 @@
   			if (module.parentModule) {
   				processHooks(test, module.parentModule);
   			}
-  			if (module.testEnvironment && objectType(module.testEnvironment[handler]) === "function") {
-  				hooks.push(test.queueHook(module.testEnvironment[handler], handler, module));
+  			if (module.hooks && objectType(module.hooks[handler]) === "function") {
+  				hooks.push(test.queueHook(module.hooks[handler], handler, module));
   			}
   		}
 
@@ -1378,7 +1535,7 @@
   			}
   		}
 
-  		notifyTestsRan(module);
+  		notifyTestsRan(module, skipped);
 
   		// Store result when possible
   		if (storage) {
@@ -1389,7 +1546,11 @@
   			}
   		}
 
+  		// After emitting the js-reporters event we cleanup the assertion data to
+  		// avoid leaking it. It is not used by the legacy testDone callbacks.
   		emit("testEnd", this.testReport.end(true));
+  		this.testReport.slimAssertions();
+
   		runLoggingCallbacks("testDone", {
   			name: testName,
   			module: moduleName,
@@ -1409,6 +1570,20 @@
   		});
 
   		if (module.testsRun === numberOfTests(module)) {
+  			logSuiteEnd(module);
+
+  			// Check if the parent modules, iteratively, are done. If that the case,
+  			// we emit the `suiteEnd` event and trigger `moduleDone` callback.
+  			var parent = module.parentModule;
+  			while (parent && parent.testsRun === numberOfTests(parent)) {
+  				logSuiteEnd(parent);
+  				parent = parent.parentModule;
+  			}
+  		}
+
+  		config.current = undefined;
+
+  		function logSuiteEnd(module) {
   			emit("suiteEnd", module.suiteReport.end(true));
   			runLoggingCallbacks("moduleDone", {
   				name: module.name,
@@ -1419,8 +1594,6 @@
   				runtime: now() - module.stats.started
   			});
   		}
-
-  		config.current = undefined;
   	},
 
   	preserveTestEnvironment: function preserveTestEnvironment() {
@@ -1431,18 +1604,16 @@
   	},
 
   	queue: function queue() {
-  		var priority,
-  		    previousFailCount,
-  		    test = this;
+  		var test = this;
 
   		if (!this.valid()) {
   			return;
   		}
 
-  		function run() {
+  		function runTest() {
 
   			// Each of these can by async
-  			synchronize([function () {
+  			ProcessingQueue.addImmediate([function () {
   				test.before();
   			}, test.hooks("before"), function () {
   				test.preserveTestEnvironment();
@@ -1455,17 +1626,26 @@
   			}]);
   		}
 
-  		previousFailCount = config.storage && +config.storage.getItem("qunit-test-" + this.module.name + "-" + this.testName);
+  		var previousFailCount = config.storage && +config.storage.getItem("qunit-test-" + this.module.name + "-" + this.testName);
 
   		// Prioritize previously failed tests, detected from storage
-  		priority = config.reorder && previousFailCount;
+  		var prioritize = config.reorder && !!previousFailCount;
 
   		this.previousFailure = !!previousFailCount;
 
-  		return synchronize(run, priority, config.seed);
+  		ProcessingQueue.add(runTest, prioritize, config.seed);
+
+  		// If the queue has already finished, we manually process the new test
+  		if (ProcessingQueue.finished) {
+  			ProcessingQueue.advance();
+  		}
   	},
 
+
   	pushResult: function pushResult(resultInfo) {
+  		if (this !== config.current) {
+  			throw new Error("Assertion occured after test had finished.");
+  		}
 
   		// Destructure of resultInfo = { result, actual, expected, message, negative }
   		var source,
@@ -1503,7 +1683,7 @@
   			throw new Error("pushFailure() assertion outside test context, was " + sourceFromStacktrace(2));
   		}
 
-  		this.assert.pushResult({
+  		this.pushResult({
   			result: false,
   			message: message || "error",
   			actual: actual || null,
@@ -1643,79 +1823,6 @@
   	return currentTest.pushFailure.apply(currentTest, arguments);
   }
 
-  // Based on Java's String.hashCode, a simple but not
-  // rigorously collision resistant hashing function
-  function generateHash(module, testName) {
-  	var hex,
-  	    i = 0,
-  	    hash = 0,
-  	    str = module + "\x1C" + testName,
-  	    len = str.length;
-
-  	for (; i < len; i++) {
-  		hash = (hash << 5) - hash + str.charCodeAt(i);
-  		hash |= 0;
-  	}
-
-  	// Convert the possibly negative integer hash code into an 8 character hex string, which isn't
-  	// strictly necessary but increases user understanding that the id is a SHA-like hash
-  	hex = (0x100000000 + hash).toString(16);
-  	if (hex.length < 8) {
-  		hex = "0000000" + hex;
-  	}
-
-  	return hex.slice(-8);
-  }
-
-  function synchronize(callback, priority, seed) {
-  	var last = !priority,
-  	    index;
-
-  	if (objectType(callback) === "array") {
-  		while (callback.length) {
-  			synchronize(callback.shift());
-  		}
-  		return;
-  	}
-
-  	if (priority) {
-  		config.queue.splice(priorityCount++, 0, callback);
-  	} else if (seed) {
-  		if (!unitSampler) {
-  			unitSampler = unitSamplerGenerator(seed);
-  		}
-
-  		// Insert into a random position after all priority items
-  		index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1));
-  		config.queue.splice(priorityCount + index, 0, callback);
-  	} else {
-  		config.queue.push(callback);
-  	}
-
-  	if (internalState.autorun && !config.blocking) {
-  		process(last);
-  	}
-  }
-
-  function unitSamplerGenerator(seed) {
-
-  	// 32-bit xorshift, requires only a nonzero seed
-  	// http://excamera.com/sphinx/article-xorshift.html
-  	var sample = parseInt(generateHash(seed), 16) || -1;
-  	return function () {
-  		sample ^= sample << 13;
-  		sample ^= sample >>> 17;
-  		sample ^= sample << 5;
-
-  		// ECMAScript has no unsigned number type
-  		if (sample < 0) {
-  			sample += 0x100000000;
-  		}
-
-  		return sample / 0x100000000;
-  	};
-  }
-
   function saveGlobal() {
   	config.pollution = [];
 
@@ -1888,24 +1995,40 @@
   	}
   }
 
-  function numberOfTests(module) {
-  	var count = module.tests.length,
-  	    modules = [].concat(toConsumableArray(module.childModules));
+  function collectTests(module) {
+  	var tests = [].concat(module.tests);
+  	var modules = [].concat(toConsumableArray(module.childModules));
 
   	// Do a breadth-first traversal of the child modules
   	while (modules.length) {
   		var nextModule = modules.shift();
-  		count += nextModule.tests.length;
+  		tests.push.apply(tests, nextModule.tests);
   		modules.push.apply(modules, toConsumableArray(nextModule.childModules));
   	}
 
-  	return count;
+  	return tests;
+  }
+
+  function numberOfTests(module) {
+  	return collectTests(module).length;
+  }
+
+  function numberOfUnskippedTests(module) {
+  	return collectTests(module).filter(function (test) {
+  		return !test.skip;
+  	}).length;
   }
 
-  function notifyTestsRan(module) {
+  function notifyTestsRan(module, skipped) {
   	module.testsRun++;
+  	if (!skipped) {
+  		module.unskippedTestsRun++;
+  	}
   	while (module = module.parentModule) {
   		module.testsRun++;
+  		if (!skipped) {
+  			module.unskippedTestsRun++;
+  		}
   	}
   }
 
@@ -1978,18 +2101,22 @@
   	}, {
   		key: "async",
   		value: function async(count) {
-  			var test$$1 = this.test,
-  			    popped = false,
+  			var test$$1 = this.test;
+
+  			var popped = false,
   			    acceptCallCount = count;
 
   			if (typeof acceptCallCount === "undefined") {
   				acceptCallCount = 1;
   			}
 
-  			test$$1.usedAsync = true;
   			var resume = internalStop(test$$1);
 
   			return function done() {
+  				if (config.current !== test$$1) {
+  					throw Error("assert.async callback called after test finished.");
+  				}
+
   				if (popped) {
   					test$$1.pushFailure("Too many calls to the `assert.async` callback", sourceFromStacktrace(2));
   					return;
@@ -2027,8 +2154,8 @@
   		value: function pushResult(resultInfo) {
 
   			// Destructure of resultInfo = { result, actual, expected, message, negative }
-  			var assert = this,
-  			    currentTest = assert instanceof Assert && assert.test || config.current;
+  			var assert = this;
+  			var currentTest = assert instanceof Assert && assert.test || config.current;
 
   			// Backwards compatibility fix.
   			// Allows the direct use of global exported assertions and QUnit.assert.*
@@ -2039,12 +2166,6 @@
   				throw new Error("assertion outside test context, in " + sourceFromStacktrace(2));
   			}
 
-  			if (currentTest.usedAsync === true && currentTest.semaphore === 0) {
-  				currentTest.pushFailure("Assertion after the final `assert.async` was resolved", sourceFromStacktrace(2));
-
-  				// Allow this assertion to continue running anyway...
-  			}
-
   			if (!(assert instanceof Assert)) {
   				assert = currentTest.assert;
   			}
@@ -2181,8 +2302,9 @@
   		key: "throws",
   		value: function throws(block, expected, message) {
   			var actual = void 0,
-  			    result = false,
-  			    currentTest = this instanceof Assert && this.test || config.current;
+  			    result = false;
+
+  			var currentTest = this instanceof Assert && this.test || config.current;
 
   			// 'expected' is optional unless doing string comparison
   			if (objectType(expected) === "string") {
@@ -2306,6 +2428,11 @@
   		});
   		QUnit.config.autostart = false;
   	}
+
+  	// For Web/Service Workers
+  	if (self$1 && self$1.WorkerGlobalScope && self$1 instanceof self$1.WorkerGlobalScope) {
+  		self$1.QUnit = QUnit;
+  	}
   }
 
   var SuiteReport = function () {
@@ -2386,8 +2513,11 @@
   			var counts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { passed: 0, failed: 0, skipped: 0, todo: 0, total: 0 };
 
   			counts = this.tests.reduce(function (counts, test) {
-  				counts[test.getStatus()]++;
-  				counts.total++;
+  				if (test.valid) {
+  					counts[test.getStatus()]++;
+  					counts.total++;
+  				}
+
   				return counts;
   			}, counts);
 
@@ -2451,27 +2581,49 @@
   // it since each module has a suiteReport associated with it.
   config.currentModule.suiteReport = globalSuite;
 
+  var moduleStack = [];
   var globalStartCalled = false;
   var runStarted = false;
 
-  var internalState = {
-  	autorun: false
-  };
-
   // Figure out if we're running the tests from a server or not
   QUnit.isLocal = !(defined.document && window.location.protocol !== "file:");
 
   // Expose the current QUnit version
-  QUnit.version = "2.2.0";
+  QUnit.version = "2.3.3";
+
+  function createModule(name, testEnvironment) {
+  	var parentModule = moduleStack.length ? moduleStack.slice(-1)[0] : null;
+  	var moduleName = parentModule !== null ? [parentModule.name, name].join(" > ") : name;
+  	var parentSuite = parentModule ? parentModule.suiteReport : globalSuite;
+
+  	var module = {
+  		name: moduleName,
+  		parentModule: parentModule,
+  		tests: [],
+  		moduleId: generateHash(moduleName),
+  		testsRun: 0,
+  		unskippedTestsRun: 0,
+  		childModules: [],
+  		suiteReport: new SuiteReport(name, parentSuite)
+  	};
+
+  	var env = {};
+  	if (parentModule) {
+  		parentModule.childModules.push(module);
+  		extend(env, parentModule.testEnvironment);
+  	}
+  	extend(env, testEnvironment);
+  	module.testEnvironment = env;
+
+  	config.modules.push(module);
+  	return module;
+  }
 
   extend(QUnit, {
   	on: on,
 
   	// Call on start of module test to prepend name to all tests
   	module: function module(name, testEnvironment, executeNow) {
-  		var module, moduleFns;
-  		var currentModule = config.currentModule;
-
   		if (arguments.length === 2) {
   			if (objectType(testEnvironment) === "function") {
   				executeNow = testEnvironment;
@@ -2479,57 +2631,40 @@
   			}
   		}
 
-  		module = createModule();
+  		var module = createModule(name, testEnvironment);
+
+  		// Move any hooks to a 'hooks' object
+  		if (module.testEnvironment) {
+  			module.hooks = {
+  				before: module.testEnvironment.before,
+  				beforeEach: module.testEnvironment.beforeEach,
+  				afterEach: module.testEnvironment.afterEach,
+  				after: module.testEnvironment.after
+  			};
+
+  			delete module.testEnvironment.before;
+  			delete module.testEnvironment.beforeEach;
+  			delete module.testEnvironment.afterEach;
+  			delete module.testEnvironment.after;
+  		}
 
-  		moduleFns = {
+  		var moduleFns = {
   			before: setHook(module, "before"),
   			beforeEach: setHook(module, "beforeEach"),
   			afterEach: setHook(module, "afterEach"),
   			after: setHook(module, "after")
   		};
 
+  		var currentModule = config.currentModule;
   		if (objectType(executeNow) === "function") {
-  			config.moduleStack.push(module);
-  			setCurrentModule(module);
+  			moduleStack.push(module);
+  			config.currentModule = module;
   			executeNow.call(module.testEnvironment, moduleFns);
-  			config.moduleStack.pop();
+  			moduleStack.pop();
   			module = module.parentModule || currentModule;
   		}
 
-  		setCurrentModule(module);
-
-  		function createModule() {
-  			var parentModule = config.moduleStack.length ? config.moduleStack.slice(-1)[0] : null;
-  			var moduleName = parentModule !== null ? [parentModule.name, name].join(" > ") : name;
-  			var parentSuite = parentModule ? parentModule.suiteReport : globalSuite;
-
-  			var module = {
-  				name: moduleName,
-  				parentModule: parentModule,
-  				tests: [],
-  				moduleId: generateHash(moduleName),
-  				testsRun: 0,
-  				childModules: [],
-  				suiteReport: new SuiteReport(name, parentSuite)
-  			};
-
-  			var env = {};
-  			if (parentModule) {
-  				parentModule.childModules.push(module);
-  				extend(env, parentModule.testEnvironment);
-  				delete env.beforeEach;
-  				delete env.afterEach;
-  			}
-  			extend(env, testEnvironment);
-  			module.testEnvironment = env;
-
-  			config.modules.push(module);
-  			return module;
-  		}
-
-  		function setCurrentModule(module) {
-  			config.currentModule = module;
-  		}
+  		config.currentModule = module;
   	},
 
   	test: test,
@@ -2664,73 +2799,16 @@
   	}
 
   	config.blocking = false;
-  	process(true);
-  }
-
-  function process(last) {
-  	function next() {
-  		process(last);
-  	}
-  	var start = now();
-  	config.depth = (config.depth || 0) + 1;
-
-  	while (config.queue.length && !config.blocking) {
-  		if (!defined.setTimeout || config.updateRate <= 0 || now() - start < config.updateRate) {
-  			if (config.current) {
-
-  				// Reset async tracking for each phase of the Test lifecycle
-  				config.current.usedAsync = false;
-  			}
-  			config.queue.shift()();
-  		} else {
-  			setTimeout(next, 13);
-  			break;
-  		}
-  	}
-  	config.depth--;
-  	if (last && !config.blocking && !config.queue.length && config.depth === 0) {
-  		done();
-  	}
-  }
-
-  function done() {
-  	var runtime,
-  	    passed,
-  	    i,
-  	    key,
-  	    storage = config.storage;
-
-  	internalState.autorun = true;
-
-  	runtime = now() - config.started;
-  	passed = config.stats.all - config.stats.bad;
-
-  	emit("runEnd", globalSuite.end(true));
-  	runLoggingCallbacks("done", {
-  		failed: config.stats.bad,
-  		passed: passed,
-  		total: config.stats.all,
-  		runtime: runtime
-  	});
-
-  	// Clear own storage items if all tests passed
-  	if (storage && config.stats.bad === 0) {
-  		for (i = storage.length - 1; i >= 0; i--) {
-  			key = storage.key(i);
-  			if (key.indexOf("qunit-test-") === 0) {
-  				storage.removeItem(key);
-  			}
-  		}
-  	}
+  	ProcessingQueue.advance();
   }
 
   function setHook(module, hookName) {
-  	if (module.testEnvironment === undefined) {
-  		module.testEnvironment = {};
+  	if (!module.hooks) {
+  		module.hooks = {};
   	}
 
   	return function (callback) {
-  		module.testEnvironment[hookName] = callback;
+  		module.hooks[hookName] = callback;
   	};
   }
 
@@ -3588,13 +3666,19 @@
 
   				message += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeText(actual) + "</pre></td></tr>";
 
-  				// Don't show diff if actual or expected are booleans
-  				if (!/^(true|false)$/.test(actual) && !/^(true|false)$/.test(expected)) {
+  				if (typeof details.actual === "number" && typeof details.expected === "number") {
+  					if (!isNaN(details.actual) && !isNaN(details.expected)) {
+  						showDiff = true;
+  						diff = details.actual - details.expected;
+  						diff = (diff > 0 ? "+" : "") + diff;
+  					}
+  				} else if (typeof details.actual !== "boolean" && typeof details.expected !== "boolean") {
   					diff = QUnit.diff(expected, actual);
+
+  					// don't show diff if there is zero overlap
   					showDiff = stripHtml(diff).length !== stripHtml(expected).length + stripHtml(actual).length;
   				}
 
-  				// Don't show diff if expected and actual are totally different
   				if (showDiff) {
   					message += "<tr class='test-diff'><th>Diff: </th><td><pre>" + diff + "</pre></td></tr>";
   				}
@@ -3691,6 +3775,7 @@
   				var todoLabel = document$$1.createElement("em");
   				todoLabel.className = "qunit-todo-label";
   				todoLabel.innerHTML = "todo";
+  				testItem.className += " todo";
   				testItem.insertBefore(todoLabel, testTitle);
   			}