(function(root){ 'use strict'; // testharness doesn't know about async test queues, // so this wrapper takes care of that /* USAGE: runParallelAsyncHarness({ // list of data to test, must be array of objects. // each object must contain a "name" property to describe the test // besides name, the object can contain whatever data you need tests: [ {name: "name of test 1", custom: "data"}, {name: "name of test 2", custom: "data"}, // ... ], // number of tests (tests, not test-cases!) to run concurrently testsPerSlice: 100, // time in milliseconds a test-run takes duration: 1000, // test-cases to run for for the test - there must be at least one // each case creates its separate async_test() instance cases: { // test case named "test1" test1: { // run as a async_test.step() this callback contains your primary assertions start: function(testCaseKey, data, options){}, // run as a async_test.step() this callback contains assertions to be run // when the test ended, immediately before teardown done: function(testCaseKey, data, options){} }, // ... } // all callbacks are optional: // invoked for individual test before it starts so you can setup the environment // like DOM, CSS, adding event listeners and such setup: function(data, options){}, // invoked after a test ended, so you can clean up the environment // like DOM, CSS, removing event listeners and such teardown: function(data, options){}, // invoked before a batch of tests ("slice") are run concurrently // tests is an array of test data objects sliceStart: function(options, tests) // invoked after a batch of tests ("slice") were run concurrently // tests is an array of test data objects sliceDone: function(options, tests) // invoked once all tests are done done: function(options){} }) */ root.runParallelAsyncHarness = function(options) { if (!options.cases) { throw new Error("Options don't contain test cases!"); } var noop = function(){}; // add a 100ms buffer to the test timeout, just in case var duration = Math.ceil(options.duration + 100); // names of individual tests var cases = Object.keys(options.cases); // run tests in a batch of slices // primarily not to overload weak devices (tablets, phones, …) // with too many tests running simultaneously var iteration = -1; var testPerSlice = options.testsPerSlice || 100; var slices = Math.ceil(options.tests.length / testPerSlice); // initialize all async test cases // Note: satisfying testharness.js needs to know all async tests before load-event options.tests.forEach(function(data, index) { data.cases = {}; cases.forEach(function(name) { data.cases[name] = async_test(data.name + " / " + name); }); }); function runLoop() { iteration++; if (iteration >= slices) { // no more slice, we're done (options.done || noop)(options); return; } // grab a slice of testss and initialize them var offset = iteration * testPerSlice; var tests = options.tests.slice(offset, offset + testPerSlice); tests.forEach(function(data) { (options.setup || noop)(data, options); }); // kick off the current slice of tests (options.sliceStart || noop)(options, tests); // perform individual "start" test-case tests.forEach(function(data) { cases.forEach(function(name) { data.cases[name].step(function() { (options.cases[name].start || noop)(data.cases[name], data, options); }); }); }); // conclude slice (possibly abort) var concludeSlice = function() { tests.forEach(function(data) { // perform individual "done" test-case cases.forEach(function(name) { data.cases[name].step(function() { (options.cases[name].done || noop)(data.cases[name], data, options); }); }); // clean up after individual test (options.teardown || noop)(data, options); // tell harness we're done with individual test-cases cases.forEach(function(name) { data.cases[name].done(); }); }); // finish the test for current slice of tests (options.sliceDone || noop)(options, tests); // next test please, give the browser 50ms to do catch its breath setTimeout(runLoop, 50); } // wait on RAF before cleanup to make sure all queued event handlers have run setTimeout(function() {requestAnimationFrame(concludeSlice)},duration); } // allow DOMContentLoaded before actually doing something setTimeout(runLoop, 100); }; })(window);