Skip to content

Commit

Permalink
Build: Refactor how we define QUnit.start() in prep for ESM
Browse files Browse the repository at this point in the history
In 05e15ba, I converted QUnit.start() to be generated by
createStartFunction(QUnit) to avoid a circular dependency.

This doesn't work in practice with ESM. See inline comment added
for details.

Ref qunitjs#1551.
  • Loading branch information
Krinkle committed Jul 24, 2024
1 parent 89c7c8c commit af64edc
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 68 deletions.
4 changes: 2 additions & 2 deletions src/core/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { on } from './events.js';
import onUncaughtException from './on-uncaught-exception.js';
import diff from './diff.js';
import version from './version.js';
import { createStartFunction } from './start.js';
import { start } from './start.js';

// The "currentModule" object would ideally be defined using the createModule()
// function. Since it isn't, add the missing suiteReport property to it now that
Expand Down Expand Up @@ -61,13 +61,13 @@ const QUnit = {

assert: Assert.prototype,
module,
start,
test,

// alias other test flavors for easy access
todo: test.todo,
skip: test.skip,
only: test.only
};
QUnit.start = createStartFunction(QUnit);

export default QUnit;
2 changes: 2 additions & 0 deletions src/core/qunit.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import QUnit from './core.js';
import { initBrowser } from './browser/browser-runner.js';
import { window, document } from './globals.js';
import { setQUnitObject } from './start.js';
import exportQUnit from './export.js';

setQUnitObject(QUnit);
exportQUnit(QUnit);

if (window && document) {
Expand Down
158 changes: 92 additions & 66 deletions src/core/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,85 +12,111 @@ function unblockAndAdvanceQueue () {
config._pq.advance();
}

// Inject the complete QUnit API for use by reporters
export function createStartFunction (QUnit) {
function doBegin () {
if (config.started) {
unblockAndAdvanceQueue();
return;
}
// There are two places where we need a complete QUnit API object inside QUnit
// 1. QUnit.start -> doBegin -> init() of reporters.
// 2. exportQUnit() to define the global variable.
//
// This was trivial before ESM, as core.js simply defines the QUnit object
// (without "start"), then passes QUnit to a "createStartFunction(QUnit)",
// and assign the result to QUnit.test. Likewise, exportQUnit() simply
// assigns the whole thing to a global variable.
//
// With ESM we don't have a way to refer to the current exports object
// while we're building it, so core.js would have to omit "QUnit.start".
// We could then import core.js in qunit.js and create and export the start
// function there. Unfortunately, the object you get from `import * as QUnit from qunit.js`
// is read-only and non-configurable, so while you can `export * from`
// and `export start` to provide a full composite to the public, you still don't
// have the complete thing internally. This could be solved with another
// intermediary file. But instead, the below gives on composition for this
// use case and instead lets the entrypoint inject the variable retroactively.
let _qunit;
export function setQUnitObject (QUnit) {
_qunit = QUnit;
}

// QUnit.config.reporters is considered writable between qunit.js and QUnit.start().
// Now, it is time to decide which reporters we'll load.
if (config.reporters.console) {
reporters.console.init(QUnit);
}
if (config.reporters.html || (config.reporters.html === undefined && window && document)) {
reporters.html.init(QUnit);
}
if (config.reporters.perf || (config.reporters.perf === undefined && window && document)) {
reporters.perf.init(QUnit);
}
if (config.reporters.tap) {
reporters.tap.init(QUnit);
}
function doBegin () {
if (config.started) {
unblockAndAdvanceQueue();
return;
}

// The test run hasn't officially begun yet
// Record the time of the test run's beginning
config.started = performance.now();
/* istanbul ignore if: private function */
if (!_qunit) {
// setQUnitObject() must be called internally by qunit.js before finalizing module exports
throw new TypeError('Missing internal QUnit reference');
}

// Delete the loose unnamed module if unused.
if (config.modules[0].name === '' && config.modules[0].tests.length === 0) {
config.modules.shift();
}
// QUnit.config.reporters is considered writable between qunit.js and QUnit.start().
// Now is the time we decide which reporters we load.
if (config.reporters.console) {
reporters.console.init(_qunit);
}
if (config.reporters.html || (config.reporters.html === undefined && window && document)) {
reporters.html.init(_qunit);
}
if (config.reporters.perf || (config.reporters.perf === undefined && window && document)) {
reporters.perf.init(_qunit);
}
if (config.reporters.tap) {
reporters.tap.init(_qunit);
}

const modulesLog = [];
for (let i = 0; i < config.modules.length; i++) {
// Don't expose the unnamed global test module to plugins.
if (config.modules[i].name !== '') {
modulesLog.push({
name: config.modules[i].name,
moduleId: config.modules[i].moduleId
});
}
}
// The test run hasn't officially begun yet
// Record the time of the test run's beginning
config.started = performance.now();

// The test run is officially beginning now
emit('runStart', runSuite.start(true));
runLoggingCallbacks('begin', {
totalTests: Test.count,
modules: modulesLog
}).then(unblockAndAdvanceQueue);
// Delete the loose unnamed module if unused.
if (config.modules[0].name === '' && config.modules[0].tests.length === 0) {
config.modules.shift();
}

return function start () {
if (config.current) {
throw new Error('QUnit.start cannot be called inside a test.');
const modulesLog = [];
for (let i = 0; i < config.modules.length; i++) {
// Don't expose the unnamed global test module to plugins.
if (config.modules[i].name !== '') {
modulesLog.push({
name: config.modules[i].name,
moduleId: config.modules[i].moduleId
});
}
if (config._runStarted) {
if (document && config.autostart) {
throw new Error('QUnit.start() called too many times. Did you call QUnit.start() in browser context when autostart is also enabled? https://qunitjs.com/api/QUnit/start/');
}
throw new Error('QUnit.start() called too many times.');
}

// The test run is officially beginning now
emit('runStart', runSuite.start(true));
runLoggingCallbacks('begin', {
totalTests: Test.count,
modules: modulesLog
}).then(unblockAndAdvanceQueue);
}

export function start () {
if (config.current) {
throw new Error('QUnit.start cannot be called inside a test.');
}
if (config._runStarted) {
if (document && config.autostart) {
throw new Error('QUnit.start() called too many times. Did you call QUnit.start() in browser context when autostart is also enabled? https://qunitjs.com/api/QUnit/start/');
}
throw new Error('QUnit.start() called too many times.');
}

config._runStarted = true;
config._runStarted = true;

// Add a slight delay to allow definition of more modules and tests.
if (document && document.readyState !== 'complete' && setTimeout) {
// In browser environments, if QUnit.start() is called very early,
// still wait for DOM ready to ensure reliable integration of reporters.
window.addEventListener('load', function () {
setTimeout(function () {
doBegin();
});
});
} else if (setTimeout) {
// Add a slight delay to allow definition of more modules and tests.
if (document && document.readyState !== 'complete' && setTimeout) {
// In browser environments, if QUnit.start() is called very early,
// still wait for DOM ready to ensure reliable integration of reporters.
window.addEventListener('load', function () {
setTimeout(function () {
doBegin();
});
} else {
});
} else if (setTimeout) {
setTimeout(function () {
doBegin();
}
};
});
} else {
doBegin();
}
}

0 comments on commit af64edc

Please sign in to comment.