From c21ceb781015fa7c1f3a6ab685f666ab32785e73 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Wed, 6 Mar 2024 01:31:16 +0000 Subject: [PATCH] Core: Add support for flat preconfig via environment/global variables --- build/prep-release.js | 6 +- docs/api/config/current.md | 2 +- docs/api/config/index.md | 110 +++++++++++++++++--- src/core/config.js | 75 ++++++++++++- src/core/processing-queue.js | 3 - src/globals.js | 8 +- src/html-runner/urlparams.js | 5 + test/integration/karma-qunit.js | 38 +++++-- test/integration/karma-qunit/karma.conf.js | 8 +- test/integration/karma-qunit/pass-config.js | 4 + 10 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 test/integration/karma-qunit/pass-config.js diff --git a/build/prep-release.js b/build/prep-release.js index 1795504b0..c7db3a21b 100644 --- a/build/prep-release.js +++ b/build/prep-release.js @@ -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 = { @@ -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( @@ -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)) ); }); } diff --git a/docs/api/config/current.md b/docs/api/config/current.md index 2944aa4a9..dfe084112 100644 --- a/docs/api/config/current.md +++ b/docs/api/config/current.md @@ -15,7 +15,7 @@ Internal object representing the currently running test. - +
type`object` (read-only)`undefined` or `object` (read-only)
diff --git a/docs/api/config/index.md b/docs/api/config/index.md index 137e9ed3f..e66727dda 100644 --- a/docs/api/config/index.md +++ b/docs/api/config/index.md @@ -2,23 +2,106 @@ 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 + + + + QUnit + + + +
+ + + + + + +``` + +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 + +``` + +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 @@ -26,15 +109,16 @@ QUnit = { }; // 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.
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.
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. diff --git a/src/core/config.js b/src/core/config.js index e601211af..ed1f7f43e 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -1,4 +1,4 @@ -import { globalThis, localSessionStorage } from '../globals'; +import { globalThis, process, localSessionStorage } from '../globals'; import { extend } from './utilities'; /** @@ -10,10 +10,16 @@ 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, @@ -21,6 +27,9 @@ const config = { // 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, @@ -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: [], @@ -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: {}, @@ -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. diff --git a/src/core/processing-queue.js b/src/core/processing-queue.js index 6306ee8f4..ca8e16a87 100644 --- a/src/core/processing-queue.js +++ b/src/core/processing-queue.js @@ -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(() => { diff --git a/src/globals.js b/src/globals.js index eb63daf9c..396b6ff4b 100644 --- a/src/globals.js +++ b/src/globals.js @@ -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; diff --git a/src/html-runner/urlparams.js b/src/html-runner/urlparams.js index f71e51c21..4b2d180a1 100644 --- a/src/html-runner/urlparams.js +++ b/src/html-runner/urlparams.js @@ -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 || []); diff --git a/test/integration/karma-qunit.js b/test/integration/karma-qunit.js index b739f1b63..60dd0308e 100644 --- a/test/integration/karma-qunit.js +++ b/test/integration/karma-qunit.js @@ -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 }); }); diff --git a/test/integration/karma-qunit/karma.conf.js b/test/integration/karma-qunit/karma.conf.js index ea8df3d8a..388f499c0 100644 --- a/test/integration/karma-qunit/karma.conf.js +++ b/test/integration/karma-qunit/karma.conf.js @@ -5,9 +5,15 @@ module.exports = function (config) { browsers: [ 'FirefoxHeadless' ], + client: process.env.KARMA_QUNIT_CONFIG ? { + qunit: { + testTimeout: 1991, + fooBar: 'xyz' + } + }: {}, frameworks: ['qunit'], files: [ - process.env.KARMA_FILES || 'pass-*.js' + process.env.KARMA_FILES || 'pass-basic.js' ], autoWatch: false, singleRun: true, diff --git a/test/integration/karma-qunit/pass-config.js b/test/integration/karma-qunit/pass-config.js new file mode 100644 index 000000000..68073a040 --- /dev/null +++ b/test/integration/karma-qunit/pass-config.js @@ -0,0 +1,4 @@ +QUnit.test('set config', function (assert) { + assert.strictEqual(QUnit.config.testTimeout, 1991, 'testTimeout'); + assert.strictEqual(QUnit.config.fooBar, 'xyz', 'fooBar'); +});