Skip to content

Commit

Permalink
Core: Support HTML/TAP preconfig via QUnit.config.reporters
Browse files Browse the repository at this point in the history
Allow disabling of HTML Reporter even if `<div id="qunit">` exists.

Ref qunitjs#1711.
  • Loading branch information
Krinkle committed Jul 22, 2024
1 parent c73e8e5 commit c4ed2a2
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 12 deletions.
2 changes: 2 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ module.exports = function (grunt) {
all: {
options: {
timeout: 30000,
console: false,
puppeteer: {
args: isCI

Expand Down Expand Up @@ -106,6 +107,7 @@ module.exports = function (grunt) {
'test/browser-runner/autostart.html',
'test/browser-runner/config-fixture-null.html',
'test/browser-runner/config-fixture-string.html',
'test/browser-runner/config-reporters-html.html',
'test/browser-runner/headless.html',
'test/browser-runner/window-onerror-preexisting-handler.html',
'test/browser-runner/window-onerror.html'
Expand Down
68 changes: 68 additions & 0 deletions docs/api/config/reporters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
layout: page-api
title: QUnit.config.reporters
excerpt: Control which reporters to enable or disable.
groups:
- config
redirect_from:
- "/config/reporters/"
version_added: "unreleased"
---

Control which reporters to enable or disable.

<table>
<tr>
<th>type</th>
<td markdown="span">`object`</td>
</tr>
<tr>
<th>default</th>
<td markdown="span">`{}`</td>
</tr>
</table>

## See also

* [Preconfig](./index.md#preconfiguration)
* [QUnit Reporter API](../callbacks/QUnit.on.md#reporter-api)

## Built-in reporters

### console

The **console** reporter, logs a JSON object for each reporter event from [`QUnit.on`](./api/callbacks/QUnit.on.md). Use this to explore or debug the Reporter API.

```
runStart {…}
testStart {…}
testEnd {…}
testStart {…}
testEnd {…}
runEnd {…}
```

### perf

The **perf** reporter.

### tap

The **tap** reporter.

### html

The **html** reporter.

## Examples

By default, the HTML Reporter is enabled in browser environments if a `<div id="qunit">` element exists. You can [disable the HTML Reporter](../../browser.md).

```js
//
QUnit.config.reporters.html = false;
```

```js
QUnit.config.reporters.html = true;
```
2 changes: 2 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ Built-in reporters:
* `tap`: [TAP compliant](https://testanything.org/) reporter.
* `console`: Log the JSON object for each reporter event from [`QUnit.on`](./api/callbacks/QUnit.on.md). Use this to explore or debug the Reporter API.

Check [`QUnit.config.reporters`](./api/config/reporters.md) for more information.

### `--require`

These modules or scripts will be required before any tests begin running.
Expand Down
14 changes: 11 additions & 3 deletions src/core/browser/browser-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,19 @@ export function initBrowser (QUnit, window, document) {

initFixture(QUnit, document);
initUrlConfig(QUnit);
QUnit.reporters.perf.init(QUnit);
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.
// * Is is important to set up HTML Reporter (if enabled) before calling QUnit.start(),
// as otherwise it will miss the first few or even all synchronous events.
//
// Priot to QUnit 3.0, the reporter was initialised here, between error handler (above),
// and start (below). As of QUnit 3.0, reporters are initialized by doBegin() within
// QUnit.start(), which is logically the same place, but decoupled from initBrowser().

function autostart () {
// Check as late as possible because if projecst set autostart=false,
// Check as late as possible because if projects set autostart=false,
// they generally do so in their own scripts, after qunit.js.
if (QUnit.config.autostart) {
QUnit.start();
Expand Down
2 changes: 1 addition & 1 deletion src/core/browser/urlparams.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function initUrlConfig (QUnit) {
// already set. This prevents internal TypeError from bad urls where keys
// could otherwise unexpectedly be set to type string or array.
//
// Given that HTML Reporter renders checkboxes based on QUnit.config
// Given that HTML Reporter sets checkbox state based on QUnit.config,
// instead of QUnit.urlParams, this also helps make sure that checkboxes
// for built-in keys are correctly shown as off if a urlParams value exists
// but was invalid and discarded by config.js.
Expand Down
17 changes: 17 additions & 0 deletions src/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ const config = {
// very useful in combination with "Hide passed tests" checked
reorder: true,

reporters: {},

// When enabled, all tests must call expect()
requireExpects: false,

Expand Down Expand Up @@ -216,6 +218,21 @@ function readFlatPreconfig (obj) {
readFlatPreconfigString(obj.qunit_config_seed, 'seed');
readFlatPreconfigStringArray(obj.qunit_config_testid, 'testId');
readFlatPreconfigNumber(obj.qunit_config_testtimeout, 'testTimeout');

const reporterKeys = {
qunit_config_reporters_console: 'console',
qunit_config_reporters_perf: 'perf',
qunit_config_reporters_tap: 'tap',
qunit_config_reporters_html: 'html'
};
for (const key in reporterKeys) {
const val = obj[key];
// Based on readFlatPreconfigBoolean
if (typeof val === 'boolean' || (typeof val === 'string' && val !== '')) {
const dest = reporterKeys[key];
config.reporters[dest] = (val === true || val === 'true' || val === '1');
}
}
}

if (process && 'env' in process) {
Expand Down
4 changes: 3 additions & 1 deletion 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 { start } from './start.js';
import { start, setQUnitForStart } 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 @@ -70,4 +70,6 @@ const QUnit = {
only: test.only
};

setQUnitForStart(QUnit);

export default QUnit;
20 changes: 16 additions & 4 deletions src/core/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,30 +46,42 @@ export function emit (eventName, data) {
}
}

export const prioritySymbol = {};

/**
* Registers a callback as a listener to the specified event.
*
* @public
* @method on
* @param {string} eventName
* @param {Function} callback
* @param {Object} [priority] Internal parameter for PerfReporter
* @return {void}
*/
export function on (eventName, callback) {
export function on (eventName, callback, priority = null) {
if (typeof eventName !== 'string') {
throw new TypeError('eventName must be a string when registering a listener');
} else if (!inArray(eventName, SUPPORTED_EVENTS)) {
}
if (!inArray(eventName, SUPPORTED_EVENTS)) {
const events = SUPPORTED_EVENTS.join(', ');
throw new Error(`"${eventName}" is not a valid event; must be one of: ${events}.`);
} else if (typeof callback !== 'function') {
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function when registering a listener');
}
if (priority && priority !== prioritySymbol) {
throw new TypeError('invalid priority parameter');
}

const listeners = config._event_listeners[eventName] || (config._event_listeners[eventName] = []);

// Don't register the same callback more than once
if (!inArray(callback, listeners)) {
listeners.push(callback);
if (priority === prioritySymbol) {
listeners.unshift(callback);
} else {
listeners.push(callback);
}

if (config._event_memory[eventName] !== undefined) {
callback(config._event_memory[eventName]);
Expand Down
3 changes: 3 additions & 0 deletions src/core/reporters/HtmlReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ export default class HtmlReporter {
// Other event handlers are added via listen() from onRunStart,
// after we know that the element exists. This reduces overhead and avoids
// potential internal errors when the HTML Reporter is disabled.
//
// TODO: Consider using prioritySignal for testStart() to increase availability
// of the HTML API for TESTID elements toward other event listeners.
this.listen = function () {
this.listen = null;
QUnit.begin(this.onBegin.bind(this));
Expand Down
7 changes: 4 additions & 3 deletions src/core/reporters/PerfReporter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { window } from '../globals.js';
import { prioritySymbol } from '../events.js';
import Logger from '../logger.js';

// TODO: Consider using globalThis instead of window, so that the reporter
Expand Down Expand Up @@ -39,11 +40,11 @@ export default class PerfReporter {
constructor (runner, options = {}) {
this.perf = options.perf || perf;

runner.on('runStart', this.onRunStart.bind(this));
runner.on('runStart', this.onRunStart.bind(this), prioritySymbol);
runner.on('runEnd', this.onRunEnd.bind(this));
runner.on('suiteStart', this.onSuiteStart.bind(this));
runner.on('suiteStart', this.onSuiteStart.bind(this), prioritySymbol);
runner.on('suiteEnd', this.onSuiteEnd.bind(this));
runner.on('testStart', this.onTestStart.bind(this));
runner.on('testStart', this.onTestStart.bind(this), prioritySymbol);
runner.on('testEnd', this.onTestEnd.bind(this));
}

Expand Down
23 changes: 23 additions & 0 deletions src/core/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,42 @@ import { emit } from './events.js';
import { window, document, setTimeout } from './globals.js';
import { runSuite } from './module.js';
import Test from './test.js';
import reporters from './reporters.js';
import { performance } from './utilities.js';

function unblockAndAdvanceQueue () {
config.blocking = false;
config._pq.advance();
}

let _qunit;

// Inject the complete QUnit API for use by reporters
export function setQUnitForStart (QUnit) {
_qunit = QUnit;
}

function doBegin () {
if (config.started) {
unblockAndAdvanceQueue();
return;
}

// 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);
}

// The test run hasn't officially begun yet
// Record the time of the test run's beginning
config.started = performance.now();
Expand Down
20 changes: 20 additions & 0 deletions test/browser-runner/config-reporters-html.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>config-reporters-html</title>
<link rel="stylesheet" href="../../src/core/qunit.css">
<script>
/* eslint-disable camelcase, no-undef */
// go headless
qunit_config_reporters_html = false;
// enable TAP
qunit_config_reporters_tap = true;
</script>
<script src="../../qunit/qunit.js"></script>
<script src="config-reporters-html.js"></script>
</head>
<body>
<div id="qunit"></div>
</body>
</html>
14 changes: 14 additions & 0 deletions test/browser-runner/config-reporters-html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-env browser */
QUnit.module('QUnit.config.reporters');

QUnit.test('read config', function (assert) {
assert.deepEqual(QUnit.config.reporters, {
html: false,
tap: true
});
});

QUnit.test('HtmlReporter disabled', function (assert) {
var children = [].slice.call(document.querySelectorAll('#qunit > *'));
assert.deepEqual(children, [], '#qunit element is empty');
});

0 comments on commit c4ed2a2

Please sign in to comment.