Skip to content

Commit

Permalink
HTML Reporter: Add support for early rendering
Browse files Browse the repository at this point in the history
When projects use the recommended structure for the HTML test suite,
in which qunit.js is loaded at the end of the `<body>` we now render
the UI immediately instead of waiting for all source code and test
suites to load first, which can take a while in large projects.

Closes #1793.
  • Loading branch information
Krinkle authored Aug 9, 2024
1 parent 276e09d commit eb93713
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 33 deletions.
7 changes: 5 additions & 2 deletions demos/qunit-onerror-early.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
<link rel="stylesheet" href="../src/core/qunit.css">
<script src="../qunit/qunit.js"></script>
<script>
QUnit.config.urlConfig.push('beginBoom');
QUnit.begin(function () {
// eslint-disable-next-line no-undef
beginBoom();
if (QUnit.config.beginBoom) {
// eslint-disable-next-line no-undef
beginBoom();
}
});

// eslint-disable-next-line no-undef
Expand Down
22 changes: 22 additions & 0 deletions src/core/browser/browser-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ export function initBrowser (QUnit, window, document) {
initFixture(QUnit, document);
initUrlConfig(QUnit);

// Unless explicitly disabled via preconfig, initialize HtmlReporter now
// (i.e. QUnit.config.reporters.html is undefined or true).
//
// This allows the UI to render instantly (since QUnit 3.0) for cases where the
// qunit.js script is after `<div id="qunit">`, which is recommended.
//
// Otherwise, we'll fallback to waiting with a blank page until window.onload,
// which is how it's always been in QUnit 1 and QUnit 2.
//
// Note that HtmlReporter constructor will only render an initial layout and
// listen to 1 event. The final decision on whether to attach event handlers
// and render the interactive UI is made from HtmlReporter#onRunStart, which
// is also where it will honor QUnit.config.reporters.html if it was set to
// false between qunit.js (here) and onRunStart.
//
// If someone explicitly sets QUnit.config.reporters.html to false via preconfig,
// but then changes it at runtime to true, that is unsupported and the reporter
// will remain disabled.
if (QUnit.config.reporters.html !== false) {
QUnit.reporters.html.init(QUnit);
}

// NOTE:
// * It is important to attach error handlers (above) before setting up reporters,
// to ensure reliable reporting of error events.
Expand Down
63 changes: 38 additions & 25 deletions src/core/reporters/HtmlReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,24 @@ export default class HtmlReporter {
this.dropdownData = null;

// We must not fallback to creating `<div id="qunit">` ourselves if it
// does not exist, because not having id="qunit" is how projects indicate
// that they wish to run QUnit headless, with their own reporters.
this.element = options.element || undefined;
// does not exist, because not having `<div id="qunit">` is how projects
// indicate that they wish to run QUnit headless, and do their own reporter.
//
// If options.element was set to HTMLElement, or if `<div id="qunit">` already
// exists (i.e. qunit.js script is placed at end of `<body>`), then we render
// an initial layout now (which is mostly inert).
// Otherwise, we wait until QUnit.start and the onRunStart event, which by
// default is called from window.onload.
//
// If no element is found now, leave this.element as undefined (unchanged).
// We will try again at onRunStart().
this.element = options.element || document.querySelector('#qunit') || undefined;
this.elementBanner = null;
this.elementDisplay = null;
this.elementTests = null;
if (this.element) {
this.appendInterface();
}

// NOTE: Only listen for "runStart" now.
// Other event handlers are added via listen() from onRunStart,
Expand All @@ -150,9 +162,12 @@ export default class HtmlReporter {
QUnit.log(this.onLog.bind(this), prioritySymbol);
QUnit.testDone(this.onTestDone.bind(this));
QUnit.on('runEnd', this.onRunEnd.bind(this));
};
this.listenError = function () {
this.listenError = null;

// It's important that we don't listen for onError until after
// this.element is found and populated by appendInterface(), as
// otherwise it will fail in appendTest() to display error details.
// We've given on() a memory for "error" events to accomodate
// late listening.
QUnit.on('error', this.onError.bind(this), prioritySymbol);
};
QUnit.on('runStart', this.onRunStart.bind(this), prioritySymbol);
Expand Down Expand Up @@ -653,7 +668,7 @@ export default class HtmlReporter {
+ '">Run all tests</a></div>';
}

appendInterface (beginDetails) {
appendInterface () {
// Since QUnit 1.3, these are created automatically.
this.element.setAttribute('role', 'main');
this.element.innerHTML =
Expand All @@ -675,9 +690,6 @@ export default class HtmlReporter {
this.elementBanner = this.element.querySelector('#qunit-banner');
this.elementDisplay = this.element.querySelector('#qunit-testresult-display');
this.elementTests = this.element.querySelector('#qunit-tests');

this.appendToolbarControls(beginDetails);
this.appendTestResultControls();
}

appendTest (name, testId, moduleName) {
Expand Down Expand Up @@ -710,12 +722,22 @@ export default class HtmlReporter {

// HTML Reporter initialization and load
onRunStart (runStart) {
if (this.element === undefined) {
this.element = document.querySelector('#qunit') || null;
}
if (this.element === null) {
if (this.config.reporters && this.config.reporters.html === false) {
// If QUnit.config.reporters.html was set to false after loading QUnit,
// then undo the initial layout (created from browser-runnner.js)
if (this.element) {
this.element.innerHTML = '';
this.element = null;
}
return;
}
if (!this.element) {
this.element = document.querySelector('#qunit') || null;
if (!this.element) {
return;
}
this.appendInterface();
}

this.stats.defined = runStart.testCounts.total;

Expand All @@ -730,18 +752,9 @@ export default class HtmlReporter {
// add entries to QUnit.config.urlConfig, which may be done asynchronously.
// https://github.com/qunitjs/qunit/issues/1657
onBegin (beginDetails) {
this.appendInterface(beginDetails);
this.appendToolbarControls(beginDetails);
this.appendTestResultControls();
this.elementDisplay.className = 'running';

// TODO: We render the UI late here from onBegin because the toolbar and module
// dropdown rely on user-defined information. If we refactor the UI to render
// most of it earlier (from the constructor if possible, with last retry during
// runStart), then we can move this listener to the listen() function. We separate it
// so that onError can safely call appendTest() and rely on `this.element` and
// `this.elementTests` being set. We could instead add the "error" listener in
// the constructor, and buffer "early error" event inside this class until the
// UI is ready, but we instead rely on QUnit.on's built-in memory for "error".
this.listenError();
}

getRerunFailedHtml (failedTests) {
Expand Down
6 changes: 3 additions & 3 deletions src/core/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ export function createStartFunction (QUnit) {

// QUnit.config.reporters is considered writable between qunit.js and QUnit.start().
// Now, it is time to decide which reporters we'll load.
//
// For config.reporters.html, refer to browser-runner.js and HtmlReporter#onRunStart.
//
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);
}
Expand Down
6 changes: 3 additions & 3 deletions test/config-reporters.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<meta charset="UTF-8">
<title>config-reporters</title>
<link rel="stylesheet" href="../src/core/qunit.css">
</head>
<body>
<div id="qunit"></div>
<script src="../qunit/qunit.js"></script>
<script>
QUnit.config.reporters.html = false;
Expand Down Expand Up @@ -37,8 +40,5 @@
assert.strictEqual(window.firstConsoleMessage, 'TAP version 13', 'first console message');
});
</script>
</head>
<body>
<div id="qunit"></div>
</body>
</html>

0 comments on commit eb93713

Please sign in to comment.