QUnit queues up all the javascript tests, then runs them. This means you get an accurate picture of how long the tests take to run sequentially, but you have to be very careful about multi-threaded execution. When testing DiSCO, I had to keep using QUnit.stop and .start() whenever I had an asynchronous event between tests.

Also, when setting breakpoints in Firebug, be careful to set them inside the test functions as your code will be operating in two different timeframes. Below t0 is the test setup timeframe and t1 the test execution timeframe.


------- |-t0--- |-t1---
        test("A Loader loaded", function() {
                equal(typeof d.process, 'function', "Loader setup with augmented closure");
        });
        QUnit.stop();
        d.loader.wait(function() {
          QUnit.start();
        });
        // test libraries loaded
        test("B Libraries loaded", function() {
                equal(d.synLoadedLibs, true, "Loader loaded libraries");
                equal(d.$.fn.jquery, '1.6.1', "Loaded correct version of jQuery");
                notEqual(window.$.fn.jquery, '1.6.1', "Loader didn't upset Drupal version of jQuery");
        });
        // test loader is still there, but libraries are not loaded
        test("C Loader loaded after reset", function() {
                // reset disco (without array, so should get wrapped)
                d.q.push('_reset');
                equal(typeof d.process, 'function', "Loader setup with augmented closure");
                equal(d.$, null, "Check no local jQuery yet");
                equal(d.synLoadedLibs, false, "Check libraries not flagged as loaded");
        });
        // test libraries loaded
        test("D Settings push before", function() {
                // setup settings before 'load'
                d.q.push(['_settings',{
                        'id' : 1,
                        'api' : '1.0',
                        'apikey' : '197c879a9d15c0f1'
                }]);
                equal(d.synLoadedLibs, false, "Check libraries still not flagged as loaded");
                // execute load (again), but delay the tests until the loader has loaded the libraries
        });
        QUnit.stop();
        d.initLib(function() {
          QUnit.start();
        });
        test("E Libraries re-loaded", function() {
                equal(d.synLoadedLibs, true, "Loader loaded libraries");
                equal(d.$.fn.jquery, '1.6.1', "Loaded correct version of jQuery");
                notEqual(window.$.fn.jquery, '1.6.1', "Loader didn't upset Drupal version of jQuery");
        });

Similarly the QUnit.stop() and .start()s have to be called in the correct timeframe. The execution order above reads something like:


|
t0 - test setup timeframe
|
|        Setup test A
|        Wait for libraries (problem)
|        Setup test B
|        Setup test C
|        Setup test D
|        Wait for libraries (problem)
|        Setup test E
|
t1 - test execution timeframe
|
|        Execute test A
|        Execute test B
|        Execute test C
|        Execute test D
|        Execute test E
|

We want to stop execution near the end of one test, process our async event, then restart it before jumping into the next test. All of that occurs in the test execution timeframe, not the test setup timeframe.

Correct code


        test("Loader loaded", function() {
                equal(typeof d.process, 'function', "Loader setup with augmented closure");
                QUnit.stop();
                d.loader.wait(function() {
                        QUnit.start();
                });
        });
        // test libraries loaded
        test("Libraries loaded", function() {
                equal(d.synLoadedLibs, true, "Loader loaded libraries");
                equal(d.$.fn.jquery, '1.6.1', "Loaded correct version of jQuery");
                notEqual(window.$.fn.jquery, '1.6.1', "Loader didn't upset Drupal version of jQuery");
        });
        // test loader is still there, but libraries are not loaded
        test("Loader loaded after reset", function() {
                // reset disco (without array, so should get wrapped)
                d.q.push('_reset');
                equal(typeof d.process, 'function', "Loader setup with augmented closure");
                equal(d.$, null, "Check no local jQuery yet");
                equal(d.synLoadedLibs, false, "Check libraries not flagged as loaded");
        });
        // test libraries loaded
        test("Settings push before", function() {
                // setup settings before 'load'
                d.q.push(['_settings',{
                        'id' : 1,
                        'api' : '1.0',
                        'apikey' : '197c879a9d15c0f1'
                }]);
                equal(d.synLoadedLibs, false, "Check libraries still not flagged as loaded");
                // execute load (again), but delay the tests until the loader has loaded the libraries
                QUnit.stop();
                d.initLib(function() {
                        QUnit.start();
                });
        });
        test("Libraries re-loaded", function() {
                equal(d.synLoadedLibs, true, "Loader loaded libraries");
                equal(d.$.fn.jquery, '1.6.1', "Loaded correct version of jQuery");
                notEqual(window.$.fn.jquery, '1.6.1', "Loader didn't upset Drupal version of jQuery");
        });

Now the execution order is correct. We're waiting for the libraries in the execution timeframe.


|
t0 - test setup timeframe
|
|        Setup test A
|        Setup test B
|        Setup test C
|        Setup test D
|        Setup test E
|
t1 - test execution timeframe
|
|        Execute test A
|        Wait for libraries
|        Execute test B
|        Execute test C
|        Execute test D
|        Wait for libraries
|        Execute test E
|

Debugging concurrent programs is hard. Fortunately javascript uses callbacks rather than busy-waiting threads. When I was puzzling this out, I kept getting different behaviours each time I hit refresh (Ctrl + F5) in the browser. The reason is that multiple schedules (execution orderings) are possible when you serialise concurrent transactions. They can interleave in many orders, so you get different results. More on that in my Database course lecture notes on Concurrency.