From 3b00e36eef797e5bf7004a7fa44b703a38f32fa6 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Mon, 10 Jun 2024 23:12:19 +0100 Subject: [PATCH] Docs: Refer to QUnit.module() callback as "scope" instead of "nested" * Reserve use of the word "nested" for when multiple modules are nested as child in a parent. In general, refer to it simply as a scope. * Move "Module scope" up as first section, followed by Hooks, and lastly Options. * Embed basic example straight into the first section instead of buried far down. * Simplify and shorten several examples to draw attention to the structure and away from the example code. --- docs/api/QUnit/module.md | 313 +++++++++++++++++--------------------- docs/upgrade-guide-2.x.md | 2 +- src/module.js | 22 +-- 3 files changed, 151 insertions(+), 186 deletions(-) diff --git a/docs/api/QUnit/module.md b/docs/api/QUnit/module.md index a605d167c..041659682 100644 --- a/docs/api/QUnit/module.md +++ b/docs/api/QUnit/module.md @@ -12,9 +12,9 @@ version_added: "1.0.0" --- `QUnit.module( name )`
-`QUnit.module( name, nested )`
+`QUnit.module( name, scope )`
`QUnit.module( name, options )`
-`QUnit.module( name, options, nested )` +`QUnit.module( name, options, scope )` Group related tests under a common label. @@ -22,14 +22,50 @@ Group related tests under a common label. |-----------|-------------| | `name` (string) | Label for this group of tests. | | [`options`](#options-object) (object) | Set hook callbacks. | -| [`nested`](#nested-scope) (function) | A scope to create nested modules and/or add hooks functionally. | +| [`scope`](#module-scope) (function) | A scope for tests, nested modules, and/or hooks. | -All tests inside a module will be grouped under that module. Tests can be added to a module using the [QUnit.test](./test.md) method. Modules help organize, select, and filter tests to run. See [§ Organizing your tests](#organizing-your-tests). +All tests inside a module will be grouped under that module. Tests can be added to a module using the [QUnit.test](./test.md) method. Modules help organize, select, and filter tests to run. See [§ Examples](#examples). -Modules can be nested inside other modules. In the output, tests are generally prefixed by the names of all parent modules. E.g. "Grandparent > Parent > Child > my test". See [§ Nested module scope](#nested-module-scope). +Modules can be nested inside other modules via a [module scope](#module-scope). In the output, tests are generally prefixed by the names of all parent modules. E.g. "Grandparent > Parent > Child > my test". The `QUnit.module.only()`, `QUnit.module.skip()`, and `QUnit.module.todo()` methods are aliases for `QUnit.module()` that apply the behaviour of [`QUnit.test.only()`](./test.only.md), [`QUnit.test.skip()`](./test.skip.md) or [`QUnit.test.todo()`](./test.todo.md) to all a module's tests at once. + + +### Module scope + +The module scope can be used to group tests under a common label. These can be nested to create child modules under a common parent module. + +The module scope is given a `hooks` object which can be used to add [hooks](#hooks). + +| parameter | description | +|-----------|-------------| +| `hooks` (object) | An object for adding hooks. | + +Example: + +```js +QUnit.module('Group A', hooks => { + QUnit.test('basic test example', assert => { + assert.true(true, 'this is fine'); + }); + + QUnit.test('basic test example 2', assert => { + assert.true(true, 'this is also fine'); + }); +}); + +QUnit.module('Group B', hooks => { + QUnit.test('basic test example 3', assert => { + assert.true(true, 'this is fine'); + }); + + QUnit.test('basic test example 4', assert => { + assert.true(true, 'this is also fine'); + }); +}); +``` + ### Hooks You can use hooks to prepare fixtures, or run other setup and teardown logic. Hooks can run around individual tests, or around a whole module. @@ -39,26 +75,32 @@ You can use hooks to prepare fixtures, or run other setup and teardown logic. Ho * `afterEach`: Run a callback after each test. * `after`: Run a callback after the last test. -You can add hooks via the `hooks` parameter of a [scoped module](#nested-scope), or in the module [`options`](#options-object) object, or globally for all tests via [QUnit.hooks](./hooks.md). +You can add hooks via the `hooks` parameter to any [module scope](#module-scope) callback, or by setting a key in the [module `options`](#options-object). You can also create global hooks across all tests, via [QUnit.hooks](./hooks.md). Hooks that are added to a module, will also apply to tests in any nested modules. -Hooks that run _before_ a test, are ordered from outer-most to inner-most, in the order that they are added. This means that a test will first run any global beforeEach hooks, then the hooks of parent modules, and finally the hooks added to the immediate module the test is a part of. Hooks that run _after_ a test, are ordered from inner-most to outer-most, in the reverse order. In other words, `before` and `beforeEach` callbacks form a [queue][], while `afterEach` and `after` form a [stack][]. +#### Hook order + +_See also [§ Example: Hooks on nested modules](#hooks-on-nested-modules)._ + +Hooks that run _before_ a test, are ordered from outer-most to inner-most, in the order that they are added. This means that a test will first run any global beforeEach hooks, then the hooks of parent modules, and finally the hooks added to the current module that the test is part of. + +Hooks that run _after_ a test, are ordered from inner-most to outer-most, in the reverse order. In other words, `before` and `beforeEach` callbacks are processed in a [queue][], while `afterEach` and `after` form a [stack][]. [queue]: https://en.wikipedia.org/wiki/Queue_%28abstract_data_type%29 [stack]: https://en.wikipedia.org/wiki/Stack_%28abstract_data_type%29 #### Hook callback -A hook callback may be an async function, and may return a Promise or any other then-able. QUnit will automatically wait for your hook's asynchronous work to finish before continuing to execute the tests. Example: [§ Async hook callback](#async-hook-callback). +A hook callback may be an async function, and may return a Promise or any other then-able. QUnit will automatically wait for your hook's asynchronous work to finish before continuing to execute the tests. [§ Example: Async hook callback](#async-hook-callback). -Each hook has access to the same `assert` object, and test context via `this`, as the [QUnit.test](./test.md) that the hook is running for. Example: [§ Using the test context](#using-the-test-context). +Each hook has access to the same `assert` object, and test context via `this`, as the [QUnit.test](./test.md) that the hook is running for. [§ Example: Using the test context](#using-the-test-context). | parameter | description | |-----------|-------------| | `assert` (object) | An [Assert](../assert/index.md) object. | -

It is discouraged to dynamically create a new [QUnit.test](./test.md) from within a hook. In order to satisfy the requirement for the `after` hook to only run once and to be the last hook in a module, QUnit may associate dynamically defined tests with the parent module instead, or as global test. It is recommended to define any dynamic tests via [`QUnit.begin()`](../callbacks/QUnit.begin.md).

+

It is discouraged to dynamically create a [QUnit.test](./test.md) from inside a hook. In order to satisfy the requirement for the `after` hook to only run once and to be the last hook in a module, QUnit may associate dynamically defined tests with the parent module instead, or as global test. It is recommended to define any dynamic tests via [`QUnit.begin()`](../callbacks/QUnit.begin.md) instead.

### Options object @@ -75,48 +117,36 @@ Properties on the module options object are copied over to the test context obje Example: [§ Hooks via module options](#hooks-via-module-options). -### Nested scope - -Modules can be nested to group tests under a common label within a parent module. - -The module scope is given a `hooks` object which can be used to procedurally add [hooks](#hooks). - -| parameter | description | -|-----------|-------------| -| `hooks` (object) | An object for adding hooks. | - -Example: [§ Nested module scope](#nested-module-scope). - ## Changelog | [QUnit 2.4](https://github.com/qunitjs/qunit/releases/tag/2.4.0) | The `QUnit.module.only()`, `QUnit.module.skip()`, and `QUnit.module.todo()` aliases were introduced. | [QUnit 2.0](https://github.com/qunitjs/qunit/releases/tag/2.0.0) | The `before` and `after` options were introduced. -| [QUnit 1.20](https://github.com/qunitjs/qunit/releases/tag/1.20.0) | The `nested` scope feature was introduced. +| [QUnit 1.20](https://github.com/qunitjs/qunit/releases/tag/1.20.0) | The `scope` feature was introduced. | [QUnit 1.16](https://github.com/qunitjs/qunit/releases/tag/1.16.0) | The `beforeEach` and `afterEach` options were introduced.
The `setup` and `teardown` options were deprecated in QUnit 1.16 and removed in QUnit 2.0. ## Examples ### Organizing your tests -If `QUnit.module` is called without a `nested` callback argument, all subsequently defined tests will be grouped into that module until another module is defined. +By default, if `QUnit.module` is called without a `scope` callback, all subsequently defined tests are automatically grouped into that module, until the next module is defined. ```js QUnit.module('Group A'); -QUnit.test('basic test example 1', function (assert) { - assert.true(true, 'this is fine'); +QUnit.test('foo', function (assert) { + assert.true(true); }); -QUnit.test('basic test example 2', function (assert) { - assert.true(true, 'this is also fine'); +QUnit.test('bar', function (assert) { + assert.true(true); }); QUnit.module('Group B'); -QUnit.test('basic test example 3', function (assert) { - assert.true(true, 'this is fine'); +QUnit.test('baz', function (assert) { + assert.true(true); }); -QUnit.test('basic test example 4', function (assert) { - assert.true(true, 'this is also fine'); +QUnit.test('quux', function (assert) { + assert.true(true); }); ``` @@ -125,52 +155,73 @@ Using modern syntax: ```js QUnit.module('Group A'); -QUnit.test('basic test example', assert => { - assert.true(true, 'this is fine'); +QUnit.test('foo', assert => { + assert.true(true); }); -QUnit.test('basic test example 2', assert => { - assert.true(true, 'this is also fine'); +QUnit.test('bar', assert => { + assert.true(true); }); QUnit.module('Group B'); -QUnit.test('basic test example 3', assert => { - assert.true(true, 'this is fine'); +QUnit.test('baz', assert => { + assert.true(true); }); -QUnit.test('basic test example 4', assert => { - assert.true(true, 'this is also fine'); +QUnit.test('quux', assert => { + assert.true(true); }); ``` -Nested module scope: +### Async hook callback ```js -QUnit.module('Group A', hooks => { - QUnit.test('basic test example', assert => { - assert.true(true, 'this is fine'); +QUnit.module('Database connection', function (hooks) { + hooks.before(async function () { + await MyDb.connect(); }); - QUnit.test('basic test example 2', assert => { - assert.true(true, 'this is also fine'); + hooks.after(async function () { + await MyDb.disconnect(); }); }); +``` -QUnit.module('Group B', hooks => { - QUnit.test('basic test example 3', assert => { - assert.true(true, 'this is fine'); - }); +Module hook with Promise: - QUnit.test('basic test example 4', assert => { - assert.true(true, 'this is also fine'); - }); +An example of handling an asynchronous `then`able Promise result in hooks. This example uses an [ES6 Promise][] interface that is fulfilled after connecting to or disconnecting from database. + +[ES6 Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise + +```js +QUnit.module('Database connection', { + before: function () { + return new Promise(function (resolve, reject) { + MyDb.connect(function (err) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }, + after: function () { + return new Promise(function (resolve, reject) { + MyDb.disconnect(function (err) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } }); ``` - - -### Set hook callbacks +### Hooks on nested modules -Use `before`/`beforeEach` hooks are queued for nested modules. `after`/`afterEach` hooks are stacked on nested modules. +_Back to [§ Hook order](#hook-order)._ ```js QUnit.module('My Group', hooks => { @@ -209,53 +260,6 @@ QUnit.module('My Group', hooks => { }); ``` -### Async hook callback - -```js -QUnit.module('Database connection', function (hooks) { - hooks.before(async function () { - await MyDb.connect(); - }); - - hooks.after(async function () { - await MyDb.disconnect(); - }); -}); -``` - -Module hook with Promise: - -An example of handling an asynchronous `then`able Promise result in hooks. This example uses an [ES6 Promise][] interface that is fulfilled after connecting to or disconnecting from database. - -[ES6 Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise - -```js -QUnit.module('Database connection', { - before: function () { - return new Promise(function (resolve, reject) { - MyDb.connect(function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - }, - after: function () { - return new Promise(function (resolve, reject) { - MyDb.disconnect(function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } -}); -``` - ### Hooks via module options ```js @@ -280,46 +284,39 @@ QUnit.module('module A', { The test context object is exposed to hook callbacks. ```js -QUnit.module('Machine Maker', { +QUnit.module('Maker', { beforeEach: function () { - this.maker = new Maker(); - this.parts = ['wheels', 'motor', 'chassis']; + this.parts = ['A', 'B']; } }); -QUnit.test('makes a robot', function (assert) { - this.parts.push('arduino'); - assert.equal(this.maker.build(this.parts), 'robot'); - assert.deepEqual(this.maker.log, ['robot']); +QUnit.test('make alphabet', function (assert) { + this.parts.push('C'); + assert.equal(this.parts.join(''), 'ABC'); }); -QUnit.test('makes a car', function (assert) { - assert.equal(this.maker.build(this.parts), 'car'); - this.maker.duplicate(); - assert.deepEqual(this.maker.log, ['car', 'car']); +QUnit.test('make music', function (assert) { + this.parts.push('B', 'A'); + assert.equal(this.parts.join(''), 'ABBA'); }); ``` -The test context is also available when using the nested scope. Beware that use of the `this` binding is not available -in arrow functions. +The test context is also available when using a module scope. Beware that use of the `this` binding is not available in arrow functions. ```js -QUnit.module('Machine Maker', hooks => { +QUnit.module('Maker', hooks => { hooks.beforeEach(function () { - this.maker = new Maker(); - this.parts = ['wheels', 'motor', 'chassis']; + this.parts = ['A', 'B']; }); - QUnit.test('makes a robot', function (assert) { - this.parts.push('arduino'); - assert.equal(this.maker.build(this.parts), 'robot'); - assert.deepEqual(this.maker.log, ['robot']); + QUnit.test('make alphabet', function (assert) { + this.parts.push('C'); + assert.equal(this.parts.join(''), 'ABC'); }); - QUnit.test('makes a car', function (assert) { - assert.equal(this.maker.build(this.parts), 'car'); - this.maker.duplicate(); - assert.deepEqual(this.maker.log, ['car', 'car']); + QUnit.test('make music', function (assert) { + this.parts.push('B', 'A'); + assert.equal(this.parts.join(''), 'ABBA'); }); }); ``` @@ -358,33 +355,17 @@ QUnit.module('Robot', hooks => { // ... }); -// Only execute this module when developing the feature, -// skipping tests from other modules. +// When developing the feature, only run these tests, +// and skip tests from other modules. QUnit.module.only('Android', hooks => { let android; hooks.beforeEach(() => { android = new Android(); }); - QUnit.test('Say hello', assert => { + QUnit.test('hello', assert => { assert.strictEqual(android.hello(), 'Hello, my name is AN-2178!'); }); - - QUnit.test('Basic conversation', assert => { - android.loadConversationData({ - Hi: 'Hello', - "What's your name?": 'My name is AN-2178.', - 'Nice to meet you!': 'Nice to meet you too!', - '...': '...' - }); - - assert.strictEqual( - android.answer("What's your name?"), - 'My name is AN-2178.' - ); - }); - - // ... }); ``` @@ -403,43 +384,27 @@ QUnit.module.skip('Android', hooks => { android = new Android(); }); - QUnit.test('Say hello', assert => { + QUnit.test('hello', assert => { assert.strictEqual(android.hello(), 'Hello, my name is AN-2178!'); }); - - QUnit.test('Basic conversation', assert => { - // ... - assert.strictEqual( - android.answer('Nice to meet you!'), - 'Nice to meet you too!' - ); - }); - - // ... }); ``` Use `QUnit.module.todo()` to denote a feature that is still under development, and is known to not yet be passing all its tests. This treats an entire module's tests as if they used [`QUnit.test.todo`](./test.todo.md) instead of [`QUnit.test`](./test.md). ```js -QUnit.module.todo('Robot', hooks => { - let robot; +QUnit.module.todo('Android', hooks => { + let android; hooks.beforeEach(() => { - robot = new Robot(); - }); - - QUnit.test('Say', assert => { - // Currently, it returns undefined - assert.strictEqual(robot.say(), "I'm Robot FN-2187"); + android = new Android(); }); - QUnit.test('Move arm', assert => { - // Move the arm to point (75, 80). Currently, each throws a NotImplementedError - robot.moveArmTo(75, 80); - assert.deepEqual(robot.getPosition(), { x: 75, y: 80 }); + QUnit.test('hello', assert => { + assert.strictEqual(android.hello(), 'Hello'); + // TODO: hello + // Actual: Goodbye + // Expected: Hello }); - - // ... }); ``` @@ -455,7 +420,7 @@ Error: Cannot add afterEach hook outside the containing module. Called on "X", instead of expected "Y". ``` -This can happen if you create a nested module and forget to specify the `hooks` parameter on the inner scope: +This can happen if you use a module scope and forget to specify the `hooks` parameter on the inner scope: ```js QUnit.module('MyGroup', (hooks) => { @@ -477,7 +442,7 @@ Another way that this might happen is if you have named them differently, or per QUnit.module('MyGroup', (hooksOuter) => { QUnit.module('Child', (hooksInner) => { hooksOuter.beforeEach(() => { - // Oops, used "hooksOuter" instead of "hooksInner"! + // ^ Oops, used "hooksOuter" instead of "hooksInner"! }); QUnit.test('example'); diff --git a/docs/upgrade-guide-2.x.md b/docs/upgrade-guide-2.x.md index 0ad52b8b9..f47ee6b31 100644 --- a/docs/upgrade-guide-2.x.md +++ b/docs/upgrade-guide-2.x.md @@ -143,7 +143,7 @@ QUnit.module('router', { }); ``` -You can also use a [nested scope](./api/QUnit/module.md#nested-scope) as of QUnit 1.20, which makes for simpler sharing of variables and associating of tests with modules. +You can also use a [module scope](./api/QUnit/module.md#module-scope) as of QUnit 1.20, which makes for simpler sharing of variables and associating of tests with modules. Example: diff --git a/src/module.js b/src/module.js index b9c091db4..bd3614c72 100644 --- a/src/module.js +++ b/src/module.js @@ -82,13 +82,13 @@ function makeSetHook (module, hookName) { }; } -function processModule (name, options, executeNow, modifiers = {}) { +function processModule (name, options, scope, modifiers = {}) { if (typeof options === 'function') { - executeNow = options; + scope = options; options = undefined; } - if (isAsyncFunction(executeNow)) { + if (isAsyncFunction(scope)) { throw new TypeError('QUnit.module() callback must not be async. For async module setup, use hooks. https://qunitjs.com/api/QUnit/module/#hooks'); } @@ -113,11 +113,11 @@ function processModule (name, options, executeNow, modifiers = {}) { const prevModule = config.currentModule; config.currentModule = module; - if (typeof executeNow === 'function') { + if (typeof scope === 'function') { moduleStack.push(module); try { - const cbReturnValue = executeNow.call(module.testEnvironment, moduleFns); + const cbReturnValue = scope.call(module.testEnvironment, moduleFns); if (cbReturnValue && typeof cbReturnValue.then === 'function') { throw new TypeError('QUnit.module() callback must not be async. For async module setup, use hooks. https://qunitjs.com/api/QUnit/module/#hooks'); } @@ -134,10 +134,10 @@ function processModule (name, options, executeNow, modifiers = {}) { let focused = false; // indicates that the "only" filter was used -export function module (name, options, executeNow) { +export function module (name, options, scope) { const ignored = focused && !isParentModuleInQueue(); - processModule(name, options, executeNow, { ignored }); + processModule(name, options, scope, { ignored }); } module.only = function (...args) { @@ -156,18 +156,18 @@ module.only = function (...args) { processModule(...args); }; -module.skip = function (name, options, executeNow) { +module.skip = function (name, options, scope) { if (focused) { return; } - processModule(name, options, executeNow, { skip: true }); + processModule(name, options, scope, { skip: true }); }; -module.todo = function (name, options, executeNow) { +module.todo = function (name, options, scope) { if (focused) { return; } - processModule(name, options, executeNow, { todo: true }); + processModule(name, options, scope, { todo: true }); };