Skip to content

Commit

Permalink
Core: Add support for flat preconfig via environment/global variables
Browse files Browse the repository at this point in the history
  • Loading branch information
Krinkle committed May 23, 2024
1 parent 75b53ff commit c21ceb7
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 31 deletions.
6 changes: 3 additions & 3 deletions build/prep-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function versionDeprecatedString (version) {
}

function formatChangelogColumn (version) {
return `| [QUnit ${version}](https://github.com/qunitjs/qunit/releases/tag/${version}) |`;
return `[QUnit ${version}](https://github.com/qunitjs/qunit/releases/tag/${version})`;
}

const Repo = {
Expand All @@ -35,7 +35,7 @@ const Repo = {
{
const UNRELEASED_ADDED = versionAddedString('unreleased');
const UNRELEASED_DEP = versionDeprecatedString('unreleased');
const UNRELEASED_CHANGELOG = '| UNRELEASED |';
const UNRELEASED_CONTENT = /\bUNRELEASED\b/g;

// Silence error from grep, which exits non-zero if no results.
const results = parseLineResults(cp.execSync(
Expand All @@ -48,7 +48,7 @@ const Repo = {
doc
.replace(UNRELEASED_ADDED, versionAddedString(version))
.replace(UNRELEASED_DEP, versionDeprecatedString(version))
.replace(UNRELEASED_CHANGELOG, formatChangelogColumn(version))
.replace(UNRELEASED_CONTENT, formatChangelogColumn(version))
);
});
}
Expand Down
2 changes: 1 addition & 1 deletion docs/api/config/current.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Internal object representing the currently running test.
<table>
<tr>
<th>type</th>
<td markdown="span">`object` (read-only)</td>
<td markdown="span">`undefined` or `object` (read-only)</td>
</tr>
</table>

Expand Down
110 changes: 97 additions & 13 deletions docs/api/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,123 @@
layout: group
group: config
title: QUnit.config
excerpt: General configuration options for QUnit.
amethyst:
robots: index
redirect_from:
- "/QUnit.config/"
- "/config/"
---

General configuration options for QUnit.
## Order

## Preconfiguring QUnit
Configurations are read in the following order:

If you load QUnit asynchronously or otherwise need to configure QUnit before it is loaded, you can predefine the configuration by creating a global variable `QUnit` with a `config` property.
1. Default values
2. Flat preconfig
3. Object preconfig
4. Runner options (URL parameters in the HTML Reporter, or CLI options in the QUnit CLI)
5. Set `QUnit.config` from your own inline or bootstrap script.

The config values specified here will be carried over to the real `QUnit.config` object. Any other properties of this object will be ignored.
## Set configuration

You can configure the test run via the `QUnit.config` object. In the HTML Runner, you can set the configuration from any script after qunit.js:

```html
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>QUnit</title>
<link rel="stylesheet" href="/lib/qunit/qunit.css">
</head>
<body>
<div id="qunit"></div>
<script src="/lib/qunit/qunit.js"></script>
<script>
QUnit.config.reorder = false;
</script>
<script src="/src/app.js"></script>
<script src="/test/bootstrap.js"></script>
<script src="/test/app.test.js"></script>
</body>
```

If you have custom plugins or want to re-use your configuration across multiple HTML test suites, you can also configure your project from an external `/test/bootstrap.js` script. Make sure to place this script before your other test files.

When using the [QUnit CLI](https://qunitjs.com/cli/), you can setup your project and configure QUnit via [`--require`](https://qunitjs.com/cli/#--require).

```bash
qunit --require ./test/bootstrap.js
```
```js
// Implicit global
// Supported everywhere, including old browsers. (But not ES strict mode.)
QUnit = {
// test/bootstrap.js
QUnit.config.noglobals = true;
QUnit.config.notrycatch = true;

const MyApp = require('../');
MyApp.setAccount('TestUser');
```

## Preconfiguration

You can predefine QUnit configuration via global variables or environment variables.

This is the recommended approach for general purpose test runners and integrations, as it provides a reliable way to override the default configuration, before `QUnit` itself is loaded. Ths can be especially useful if the end-user may load QUnit asynchronously, or through custom means, where it would be difficult to inject your overrides at the "right" time (after QUnit loads, but before any user code).

Preconfig allows integrations to declare configuration without needing to embed, wrap, adapt, modify, or otherwise control the loading of QUnit itself. This in turn allows integrations to easily support and re-use standalone HTML test suites, that developers can also run and debug in the browser using their normal tooling, without depending on a installing or setting up a specific integration system.

### Flat preconfig

*Version added: UNRELEASED*.

Flat preconfig allows multiple integrations to seemlessly collaborate, without the risk of projects accidentally unsetting an override (as is the case with Object preconfig).

In the browser context (HTML Runner), global variables that start with `qunit_config_` may override the default value of a configuration option. The following inline script (before loading QUnit), is equivalent to setting `QUnit.config.hidepassed = true;` and `QUnit.config.seed = 'd84af39036';`

```html
<script>
qunit_config_hidepassed = true;
qunit_config_seed = 'd84af39036';
</script>
```

In Node.js, the same can also be done via environment variables. The variables are case-sensitive and must be in lowercase. Environment variables may set boolean values as the string `true` or `false`.

```bash
export qunit_config_noglobals=true
export qunit_config_seed=d84af39036
export qunit_config_filter=foo
```

Configuration options that are read-only, internal/undocumented, or that require an object value (such as [`QUnit.config.storage`](./storage.md)) cannot be set via environment variables. Options that require an array of strings will be converted to an array holding the given string.

### Object preconfig

*Version added: [2.1.0](https://github.com/qunitjs/qunit/releases/tag/2.1.0)*.

If a `QUnit` placeholder variable exists with a `config` object, then any properties on this object will be carried over to the real `QUnit.config` object once it exists. Any other properties on this object are ignored.

```js
// Isomorphic global
// For modern browsers, SpiderMonkey, and Node.js (including strict mode).
globalThis.QUnit = {
config: {
autostart: false,
maxDepth: 12
}
};

// Browser global
// For all browsers (including strict mode and old browsers)
// Supported in all browsers (including old browsers, and strict mode).
window.QUnit = { /* .. */ };

// Isomorphic global
// For modern browsers, SpiderMonkey, and Node.js (incl. strict mode).
globalThis.QUnit = { /* .. */ };
// Implicit global
// Supported everywhere, including old browsers. (But not ES strict mode.)
QUnit = { /* .. */ };
```

### Changelog

| [QUnit 2.18.1](https://github.com/qunitjs/qunit/releases/tag/2.18.1) | Preconfig support added for SpiderMonkey and other environments.<br/>Previously, it was limited to the browser environment.
| [QUnit 2.1.0](https://github.com/qunitjs/qunit/releases/tag/2.1.0) | Preconfig feature introduced.
| UNRELEASED | Added flat preconfig.
| [QUnit 2.18.1](https://github.com/qunitjs/qunit/releases/tag/2.18.1) | Added object preconfig support for Node.js, SpiderMonkey, and other environments.<br/>Previously, it was limited to the browser environment.
| [QUnit 2.1.0](https://github.com/qunitjs/qunit/releases/tag/2.1.0) | Introduce object preconfig feature.
75 changes: 74 additions & 1 deletion src/core/config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { globalThis, localSessionStorage } from '../globals';
import { globalThis, process, localSessionStorage } from '../globals';
import { extend } from './utilities';

/**
Expand All @@ -10,17 +10,26 @@ const config = {
// HTML Reporter: Modify document.title when suite is done
altertitle: true,

// TODO: Move here from /src/core.js in QUnit 3.
// autostart: true,

// HTML Reporter: collapse every test except the first failing test
// If false, all failing tests will be expanded
collapse: true,

// TODO: Make explicit in QUnit 3.
// current: undefined,

// whether or not to fail when there are zero tests
// defaults to `true`
failOnZeroTests: true,

// Select by pattern or case-insensitive substring match against "moduleName: testName"
filter: undefined,

// TODO: Make explicit in QUnit 3.
// fixture: undefined,

// Depth up-to which object will be dumped
maxDepth: 5,

Expand All @@ -40,11 +49,22 @@ const config = {
// By default, scroll to top of the page when suite is done
scrolltop: true,

// TODO: Make explicit in QUnit 3.
// seed: undefined,

// The storage module to use for reordering tests
storage: localSessionStorage,

testId: undefined,

// The updateRate controls how often QUnit will yield the main thread
// between tests. This is mainly for the benefit of the HTML Reporter,
// so that the browser can visually paint DOM changes with test results.
// This also helps avoid causing browsers to prompt a warning about
// long-running scripts.
// TODO: Move here from /src/core.js in QUnit 3.
// updateRate: 1000,

// HTML Reporter: List of URL parameters that are given visual controls
urlConfig: [],

Expand Down Expand Up @@ -102,6 +122,10 @@ const config = {
// Internal: ProcessingQueue singleton, created in /src/core.js
pq: null,

// Internal: Created in /src/core.js
// TODO: Move definitions here in QUnit 3.0.
// started: 0,

// Internal state
blocking: true,
callbacks: {},
Expand All @@ -110,6 +134,55 @@ const config = {
stats: { all: 0, bad: 0, testCount: 0 }
};

function readFlatPreconfigBoolean (val, dest) {
if (typeof val === 'boolean' || (typeof val === 'string' && val !== '')) {
config[dest] = (val === true || val === 'true');
}
}

function readFlatPreconfigNumber (val, dest) {
if (typeof val === 'number' || (typeof val === 'string' && /^[0-9]+$/.test(val))) {
config[dest] = +val;
}
}

function readFlatPreconfigString (val, dest) {
if (typeof val === 'string' && val !== '') {
config[dest] = val;
}
}

function readFlatPreconfigStringArray (val, dest) {
if (typeof val === 'string' && val !== '') {
config[dest] = [val];
}
}

function readFlatPreconfig (obj) {
readFlatPreconfigBoolean(obj.qunit_config_altertitle, 'altertitle');
readFlatPreconfigBoolean(obj.qunit_config_autostart, 'autostart');
readFlatPreconfigBoolean(obj.qunit_config_collapse, 'collapse');
readFlatPreconfigBoolean(obj.qunit_config_failonzerotests, 'failOnZeroTests');
readFlatPreconfigString(obj.qunit_config_filter, 'filter');
readFlatPreconfigString(obj.qunit_config_fixture, 'fixture');
readFlatPreconfigBoolean(obj.qunit_config_hidepassed, 'hidepassed');
readFlatPreconfigNumber(obj.qunit_config_maxDepth, 'maxDepth');
readFlatPreconfigString(obj.qunit_config_module, 'module');
readFlatPreconfigStringArray(obj.qunit_config_moduleId, 'moduleId');
readFlatPreconfigBoolean(obj.qunit_config_noglobals, 'noglobals');
readFlatPreconfigBoolean(obj.qunit_config_notrycatch, 'notrycatch');
readFlatPreconfigBoolean(obj.qunit_config_reorder, 'reorder');
readFlatPreconfigBoolean(obj.qunit_config_requireexpects, 'requireExpects');
readFlatPreconfigBoolean(obj.qunit_config_scrolltop, 'scrolltop');
readFlatPreconfigStringArray(obj.qunit_config_testId, 'testId');
readFlatPreconfigNumber(obj.qunit_config_testTimeout, 'testTimeout');
}

if (process && 'env' in process) {
readFlatPreconfig(process.env);
}
readFlatPreconfig(globalThis);

// Apply a predefined QUnit.config object
//
// Ignore QUnit.config if it is a QUnit distribution instead of preconfig.
Expand Down
3 changes: 0 additions & 3 deletions src/core/processing-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,6 @@ class ProcessingQueue {
if (this.taskQueue.length && !config.blocking) {
const elapsedTime = performance.now() - start;

// The updateRate ensures that a user interface (HTML Reporter) can be updated
// at least once every second. This can also prevent browsers from prompting
// a warning about long running scripts.
if (!setTimeout || config.updateRate <= 0 || elapsedTime < config.updateRate) {
const task = this.taskQueue.shift();
Promise.resolve(task()).then(() => {
Expand Down
8 changes: 6 additions & 2 deletions src/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,15 @@ function getGlobalThis () {
// to change getGlobalThis and use the same (generated) variable name there.
const g = getGlobalThis();
export { g as globalThis };
export const window = g.window;

// These optional globals are undefined in one or more environments:
// modern browser, old browser, Node.js, SpiderMonkey.
// Calling code must check these for truthy-ness before use.
export const console = g.console;
export const setTimeout = g.setTimeout;
export const clearTimeout = g.clearTimeout;

export const process = g.process;
export const window = g.window;
export const document = window && window.document;
export const navigator = window && window.navigator;

Expand Down
5 changes: 5 additions & 0 deletions src/html-runner/urlparams.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import { window } from '../globals';
}

const urlParams = getUrlParams();

// TODO: Move to /src/core/ in QUnit 3
// TODO: Document this as public API (read-only)
QUnit.urlParams = urlParams;

// TODO: Move to /src/core/config.js in QUnit 3,
// in accordance with /docs/api/config.index.md#order
QUnit.config.filter = urlParams.filter;
QUnit.config.module = urlParams.module;
QUnit.config.moduleId = [].concat(urlParams.moduleId || []);
Expand Down
38 changes: 31 additions & 7 deletions test/integration/karma-qunit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,42 @@ QUnit.module('karma-qunit', {
}
});

QUnit.test('passing test', assert => {
const expected = `
QUnit.test.each('passing test', {
basic: ['', `
INFO [karma-server]: Karma server started at
INFO [launcher]: Launching browsers FirefoxHeadless with concurrency unlimited
INFO [launcher]: Starting browser FirefoxHeadless
INFO [Firefox]: Connected on socket
...
Firefox: Executed 3 of 3 SUCCESS
`.trim();
const actual = normalize(
cp.execSync('npm test', { cwd: DIR, env: { PATH: process.env.PATH }, encoding: 'utf8' })
);
Firefox: Executed 3 of 3 SUCCESS`
],
config: ['pass-config.js', `
INFO [karma-server]: Karma server started at
INFO [launcher]: Launching browsers FirefoxHeadless with concurrency unlimited
INFO [launcher]: Starting browser FirefoxHeadless
INFO [Firefox]: Connected on socket
.
Firefox: Executed 1 of 1 SUCCESS`, { KARMA_QUNIT_CONFIG: '1' }
]
}, (assert, [file, expected, env = {}]) => {
expected = expected.trim();
let ret;
try {
ret = cp.execSync('npm test', {
cwd: DIR,
env: {
PATH: process.env.PATH,
KARMA_FILES: file,
...env
},
encoding: 'utf8'
});
} catch (e) {
const actual = normalize(e.stdout);
assert.pushResult({ result: false, actual, expected });
return;
}
const actual = normalize(ret);
assert.pushResult({ result: actual.includes(expected), actual, expected });
});

Expand Down
Loading

0 comments on commit c21ceb7

Please sign in to comment.