1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246 |
x4
x4
x127
x127
x127
x127
x127
x127
x127
x5
x5
x5
x127
x127
x127
x127
x127
x127
x121
x121
x121
x121
x121
x115
x114
x115
x115
x32
x32
x32
x121
x121
x115
x115
x115
x114
x114
x114
x114
x26
x26
x26
x114
x115
x249
x249
x121
x121
x121
x121
x34
x34
x121
x121
x52
x52
x121
x121
x31
x31
x121
x121
x27
x27
x27
x121
x121
x121
x121
x127
x127
x4
x4
x4
x4
x2
x4
x4
x4
x4
x4
x3
x3
x4
x4
x4
x4
x4
x4
x3
x3
x4
x6
x6
x6
x4
|
|
import { afterAll, beforeAll, describe } from 'jsr:@std/testing@^1.0.17/bdd';
import type Assert from '../shared/assert.ts';
import ModuleContext from '../shared/module-context.ts';
import type { HookFn, HooksObject } from '../types.ts';
/**
* Defines a test module (suite) for Deno's BDD test runner.
*
* Wraps `describe()` from `@std/testing/bdd` and sets up the QUnit lifecycle
* (before/beforeEach/afterEach/after hooks, assertion counting, steps tracking).
*
* @param {string} moduleName - Name of the test suite
* @param {object} [runtimeOptions] - Optional Deno BDD options forwarded to `describe()`
* (e.g. `{ concurrency: false }`, `{ permissions: { read: true } }`)
* @param {function} moduleContent - Callback that defines tests and hooks via `hooks.before`,
* `hooks.beforeEach`, `hooks.afterEach`, `hooks.after`
* @returns {void}
* @example
* ```js ignore
* import { module, test } from "qunitx";
*
* module("Math", (hooks) => {
* hooks.before((assert) => {
* assert.step("before hook ran");
* });
*
* test("addition", (assert) => {
* assert.equal(2 + 2, 4);
* });
* });
* ```
*/
function module(moduleName: string, moduleContent: (hooks: HooksObject<Assert>, meta: { moduleName: string; options: unknown; context: Record<string, unknown> }) => void): void;
/** Defines a test module (suite) with optional Deno BDD runtime options forwarded to `describe()`. */
function module(moduleName: string, runtimeOptions: object, moduleContent: (hooks: HooksObject<Assert>, meta: { moduleName: string; options: unknown; context: Record<string, unknown> }) => void): void;
function module(
moduleName: string,
runtimeOptions: object | ((hooks: HooksObject<Assert>) => void),
moduleContent?: (hooks: HooksObject<Assert>, meta: { moduleName: string; options: unknown; context: Record<string, unknown> }) => void,
): void {
const targetRuntimeOptions = moduleContent ? runtimeOptions as object : {};
const { skip } = targetRuntimeOptions as { skip?: boolean | string };
// If skip is set, register a skipped describe() without creating a ModuleContext.
// The ModuleContext constructor pushes to currentModuleChain; the matching pop()
// lives inside the describe callback. If the runtime skips the callback, the pop
// never runs and corrupts the chain for all subsequent modules.
// Deno uses `ignore` instead of `skip`.
if (skip) {
describe(moduleName, { ignore: true }, function () {});
return;
}
const targetModuleContent = (moduleContent ?? runtimeOptions) as (hooks: HooksObject<Assert>, meta: { moduleName: string; options: unknown }) => unknown;
// Before the ModuleContext exists: its constructor pushes to currentModuleChain.
ModuleContext.assertCallbackIsSyncFunction(targetModuleContent);
const moduleContext = new ModuleContext(moduleName);
if ((targetRuntimeOptions as { only?: boolean }).only) moduleContext.focused = true;
// Deno's own `only` drops a nested suite that is not `only` itself, even inside one
// that is; QUnit runs everything inside an `only` module, so pass it down.
const describeOptions = moduleContext.focused ? { ...targetRuntimeOptions, only: true } : { ...targetRuntimeOptions };
describe(moduleName, describeOptions, function () {
const beforeHooks: HookFn<Assert>[] = [];
const afterHooks: HookFn<Assert>[] = [];
// before/after hooks run with the module's own context, as in QUnit 3: what a
// before() sets is inherited by every test, and after() sees only module state,
// never what the last test wrote to its own context.
const userContext = moduleContext.userContext;
const hookMeta = { context: userContext };
beforeAll(async () => {
// Skipped when every test in the module is (QUnit runs before on the first test).
if (!moduleContext.hasTestsToRun) return;
// before() assertions are attributed to the first direct test only (matching QUnit's model).
const firstTest = moduleContext.tests[0];
const beforeAssert = firstTest ? firstTest.assert! : moduleContext.assert!;
for (const hook of beforeHooks) {
await hook.call(userContext, beforeAssert, hookMeta);
await beforeAssert.waitForAsyncOps();
}
});
afterAll(async () => {
const allAsyncOps = moduleContext.tests.flatMap((t) => t.asyncOps);
if (allAsyncOps.length > 0) await Promise.all(allAsyncOps);
if (moduleContext.hasTestsToRun) {
const tests = moduleContext.tests;
const lastTest = tests[tests.length - 1];
const afterAssert = lastTest ? lastTest.assert! : moduleContext.assert!;
// Indexed reverse iteration avoids `afterHooks.toReversed()`'s per-call alloc.
for (let i = afterHooks.length - 1; i >= 0; i--) {
await afterHooks[i]!.call(userContext, afterAssert, hookMeta);
await afterAssert.waitForAsyncOps();
}
}
for (const testCtx of moduleContext.tests) {
testCtx.finish();
}
});
try {
ModuleContext.assertNotPromise(targetModuleContent.call(userContext, {
before(beforeFn) {
moduleContext.assertExecutionIsInsideTestModuleFor('before');
beforeHooks.push(beforeFn);
},
beforeEach(beforeEachFn) {
moduleContext.assertExecutionIsInsideTestModuleFor('beforeEach');
moduleContext.beforeEachHooks.push(beforeEachFn);
},
afterEach(afterEachFn) {
moduleContext.assertExecutionIsInsideTestModuleFor('afterEach');
moduleContext.afterEachHooks.push(afterEachFn);
},
after(afterFn) {
moduleContext.assertExecutionIsInsideTestModuleFor('after');
afterHooks.push(afterFn);
}
}, { moduleName, options: targetRuntimeOptions, context: userContext }));
} finally {
// Even when the callback throws, so the modules declared after it nest correctly.
ModuleContext.currentModuleChain.pop();
}
});
}
/**
* Skips all tests inside a module. Equivalent to `QUnit.module.skip`.
* The module is registered as ignored by Deno's runner; no test bodies run.
*
* @param {string} moduleName - Name of the module to skip.
* @param {function} [_moduleContent] - Optional body (ignored — no tests run).
* @example
* ```js ignore
* import { module, test } from "qunitx";
*
* module.skip("Math — not yet implemented", () => {
* test("addition", (assert) => {
* assert.equal(1 + 1, 2);
* });
* });
* ```
*/
module.skip = function skipModule(moduleName: string, _moduleContent?: unknown): void {
describe(moduleName, { ignore: true }, function () {});
};
/**
* Marks all tests inside a module as todo. Equivalent to `QUnit.module.todo`.
* The module is registered as ignored by Deno's runner; no test bodies run.
* Deno has no native "todo module" concept, so this maps to ignore.
*
* @param {string} moduleName - Name of the module to mark as todo.
* @param {function} [_moduleContent] - Optional body (ignored — no tests run).
* @example
* ```js ignore
* import { module, test } from "qunitx";
*
* module.todo("Math — not yet implemented", () => {
* test("addition", (assert) => {
* assert.equal(1 + 1, 2);
* });
* });
* ```
*/
module.todo = function todoModule(moduleName: string, _moduleContent?: unknown): void {
describe(moduleName, { ignore: true }, function () {});
};
/**
* Runs only this module, and any other `only` module or test: every other test is
* skipped. Equivalent to `QUnit.module.only`, and to passing `{ only: true }`.
* Deno runs only these, and then fails the run on purpose, so that an `only` is
* not committed by accident.
*
* @param {string} moduleName - Name of the module.
* @param {object} [runtimeOptions] - Optional options, as for {@linkcode module}.
* @param {function} moduleContent - Callback that defines tests and hooks.
* @example
* ```js ignore
* import { module, test } from "qunitx";
*
* module.only("Math — the one being worked on", () => {
* test("addition", (assert) => {
* assert.equal(1 + 1, 2);
* });
* });
* ```
*/
module.only = function onlyModule(
moduleName: string,
runtimeOptionsOrContent: object | ModuleContent,
moduleContent?: ModuleContent,
): void {
const [options, content] = getModuleArguments(runtimeOptionsOrContent, moduleContent);
module(moduleName, { ...options, only: true }, content);
};
/**
* Defines a module that runs only when `condition` is truthy, and is skipped otherwise.
* Equivalent to `QUnit.module.if`.
*
* @param {string} moduleName - Name of the module.
* @param {unknown} condition - Whether to run the module's tests.
* @param {object} [runtimeOptions] - Optional options, as for {@linkcode module}.
* @param {function} moduleContent - Callback that defines tests and hooks.
* @example
* ```js ignore
* import { module, test } from "qunitx";
*
* module.if("Clipboard", typeof navigator !== "undefined", () => {
* test("has the API", (assert) => {
* assert.strictEqual(typeof navigator.clipboard, "object");
* });
* });
* ```
*/
module.if = function ifModule(
moduleName: string,
condition: unknown,
runtimeOptionsOrContent: object | ModuleContent,
moduleContent?: ModuleContent,
): void {
const [options, content] = getModuleArguments(runtimeOptionsOrContent, moduleContent);
module(moduleName, condition ? options : { ...options, skip: true }, content);
};
type ModuleContent = (hooks: HooksObject<Assert>, meta: { moduleName: string; options: unknown; context: Record<string, unknown> }) => void;
function getModuleArguments(optionsOrContent: object | ModuleContent, content?: ModuleContent): [object, ModuleContent] {
return content ? [optionsOrContent as object, content] : [{}, optionsOrContent as ModuleContent];
}
// Exported here rather than on the declarations above: `export default function` on an
// overload set that also carries a namespace merge (.skip/.todo) emits a .d.ts with two
// `export default` statements, which is invalid TypeScript (TS2528/TS2652/TS2383) and
// breaks consumers that typecheck with `skipLibCheck: false`.
export default module;
export type { Assert };
export type { HookFn, HooksObject, PushResultInfo } from '../types.ts';
|