diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 4a0fb651a..9cfd65d19 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -182,12 +182,51 @@ $(() => (async () => { }], }); - // This loads the test specs serially. While it is technically possible to load them in parallel, - // the code would be very complex (it involves wrapping require.define(), configuring - // require-kernel to use the wrapped .define() via require.setGlobalKeyPath(), and using the - // asynchronous form of require()). In addition, the performance gains would be minimal because - // require-kernel only loads 2 at a time by default. (Increasing the default could cause problems - // because browsers like to limit the number of concurrent fetches.) + // Speed up tests by loading test definitions in parallel. Approach: Define a new global object + // that has a define() method, which is a wrapper around window.require.define(). The wrapper + // mutates the module definition function to temporarily replace Mocha's functions with + // placeholders. The placeholders make it possible to defer the actual Mocha function calls until + // after the modules are all loaded in parallel. require.setGlobalKeyPath() is used to coax + // require-kernel into using the wrapper define() method instead of require.define(). + + // Per-module log of attempted Mocha function calls. Key is module path, value is an array of + // [functionName, argsArray] arrays. + const mochaCalls = new Map(); + const mochaFns = + ['describe', 'context', 'it', 'specify', 'before', 'after', 'beforeEach', 'afterEach']; + window.testRunnerRequire = { + define(...args) { + if (args.length === 2) args = [{[args[0]]: args[1]}]; + if (args.length !== 1) throw new Error('unexpected args passed to testRunnerRequire.define'); + const [origDefs] = args; + const defs = {}; + for (const [path, origDef] of Object.entries(origDefs)) { + defs[path] = function (require, exports, module) { + const calls = []; + mochaCalls.set(module.id.replace(/\.js$/, ''), calls); + // Backup Mocha functions. Note that because modules can require other modules, these + // backups might be placeholders, not the actual Mocha functions. + const backups = {}; + for (const fn of mochaFns) { + // Note: Test specs can require other modules, so window[fn] might be a placeholder + // function, not the actual Mocha function. + backups[fn] = window[fn]; + window[fn] = (...args) => calls.push([fn, args]); + } + try { + return origDef.call(this, require, exports, module); + } finally { + Object.assign(window, backups); + } + }; + } + return require.define(defs); + }, + }; + require.setGlobalKeyPath('testRunnerRequire'); + // Increase fetch parallelism to speed up test spec loading. (Note: The browser might limit to a + // lower value -- this is just an upper limit.) + require.setRequestMaximum(20); const $log = $('
').text(`${err.stack || err}`)); + throw err; + } + $msg.append(' done'); + await incrementBar(); + })); + require.setGlobalKeyPath('require'); + delete window.testRunnerRequire; for (const spec of specs) { const desc = makeDesc(spec); - const $msg = appendToLog(`Loading ${desc}...`); + const $msg = appendToLog(`Executing ${desc}...`); try { - describe(desc, function () { require(spec); }); + describe(desc, function () { + for (const [fn, args] of mochaCalls.get(spec)) window[fn](...args); + }); } catch (err) { $msg.append($('').css('color', 'red').text(' FAILED')); appendToLog($('').text(`${err.stack || err}`));