modestbench 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +45 -2
  3. package/dist/adapters/ava-adapter.cjs +421 -0
  4. package/dist/adapters/ava-adapter.cjs.map +1 -0
  5. package/dist/adapters/ava-adapter.d.cts +39 -0
  6. package/dist/adapters/ava-adapter.d.cts.map +1 -0
  7. package/dist/adapters/ava-adapter.d.ts +39 -0
  8. package/dist/adapters/ava-adapter.d.ts.map +1 -0
  9. package/dist/adapters/ava-adapter.js +384 -0
  10. package/dist/adapters/ava-adapter.js.map +1 -0
  11. package/dist/adapters/ava-hooks.cjs +66 -0
  12. package/dist/adapters/ava-hooks.cjs.map +1 -0
  13. package/dist/adapters/ava-hooks.d.cts +24 -0
  14. package/dist/adapters/ava-hooks.d.cts.map +1 -0
  15. package/dist/adapters/ava-hooks.d.ts +24 -0
  16. package/dist/adapters/ava-hooks.d.ts.map +1 -0
  17. package/dist/adapters/ava-hooks.js +61 -0
  18. package/dist/adapters/ava-hooks.js.map +1 -0
  19. package/dist/adapters/ava-register.cjs +16 -0
  20. package/dist/adapters/ava-register.cjs.map +1 -0
  21. package/dist/adapters/ava-register.d.cts +11 -0
  22. package/dist/adapters/ava-register.d.cts.map +1 -0
  23. package/dist/adapters/ava-register.d.ts +11 -0
  24. package/dist/adapters/ava-register.d.ts.map +1 -0
  25. package/dist/adapters/ava-register.js +14 -0
  26. package/dist/adapters/ava-register.js.map +1 -0
  27. package/dist/adapters/mocha-adapter.cjs +254 -0
  28. package/dist/adapters/mocha-adapter.cjs.map +1 -0
  29. package/dist/adapters/mocha-adapter.d.cts +26 -0
  30. package/dist/adapters/mocha-adapter.d.cts.map +1 -0
  31. package/dist/adapters/mocha-adapter.d.ts +26 -0
  32. package/dist/adapters/mocha-adapter.d.ts.map +1 -0
  33. package/dist/adapters/mocha-adapter.js +217 -0
  34. package/dist/adapters/mocha-adapter.js.map +1 -0
  35. package/dist/adapters/node-test-adapter.cjs +335 -0
  36. package/dist/adapters/node-test-adapter.cjs.map +1 -0
  37. package/dist/adapters/node-test-adapter.d.cts +41 -0
  38. package/dist/adapters/node-test-adapter.d.cts.map +1 -0
  39. package/dist/adapters/node-test-adapter.d.ts +41 -0
  40. package/dist/adapters/node-test-adapter.d.ts.map +1 -0
  41. package/dist/adapters/node-test-adapter.js +298 -0
  42. package/dist/adapters/node-test-adapter.js.map +1 -0
  43. package/dist/adapters/node-test-hooks.cjs +72 -0
  44. package/dist/adapters/node-test-hooks.cjs.map +1 -0
  45. package/dist/adapters/node-test-hooks.d.cts +24 -0
  46. package/dist/adapters/node-test-hooks.d.cts.map +1 -0
  47. package/dist/adapters/node-test-hooks.d.ts +24 -0
  48. package/dist/adapters/node-test-hooks.d.ts.map +1 -0
  49. package/dist/adapters/node-test-hooks.js +67 -0
  50. package/dist/adapters/node-test-hooks.js.map +1 -0
  51. package/dist/adapters/node-test-register.cjs +7 -0
  52. package/dist/adapters/node-test-register.cjs.map +1 -0
  53. package/dist/adapters/node-test-register.d.cts +2 -0
  54. package/dist/adapters/node-test-register.d.cts.map +1 -0
  55. package/dist/adapters/node-test-register.d.ts +2 -0
  56. package/dist/adapters/node-test-register.d.ts.map +1 -0
  57. package/dist/adapters/node-test-register.js +5 -0
  58. package/dist/adapters/node-test-register.js.map +1 -0
  59. package/dist/adapters/types.cjs +152 -0
  60. package/dist/adapters/types.cjs.map +1 -0
  61. package/dist/adapters/types.d.cts +112 -0
  62. package/dist/adapters/types.d.cts.map +1 -0
  63. package/dist/adapters/types.d.ts +112 -0
  64. package/dist/adapters/types.d.ts.map +1 -0
  65. package/dist/adapters/types.js +148 -0
  66. package/dist/adapters/types.js.map +1 -0
  67. package/dist/cli/commands/init.cjs +21 -17
  68. package/dist/cli/commands/init.cjs.map +1 -1
  69. package/dist/cli/commands/init.d.cts.map +1 -1
  70. package/dist/cli/commands/init.d.ts.map +1 -1
  71. package/dist/cli/commands/init.js +21 -17
  72. package/dist/cli/commands/init.js.map +1 -1
  73. package/dist/cli/commands/run.cjs +6 -2
  74. package/dist/cli/commands/run.cjs.map +1 -1
  75. package/dist/cli/commands/run.js +6 -2
  76. package/dist/cli/commands/run.js.map +1 -1
  77. package/dist/cli/commands/test.cjs +392 -0
  78. package/dist/cli/commands/test.cjs.map +1 -0
  79. package/dist/cli/commands/test.d.cts +38 -0
  80. package/dist/cli/commands/test.d.cts.map +1 -0
  81. package/dist/cli/commands/test.d.ts +38 -0
  82. package/dist/cli/commands/test.d.ts.map +1 -0
  83. package/dist/cli/commands/test.js +388 -0
  84. package/dist/cli/commands/test.js.map +1 -0
  85. package/dist/cli/index.cjs +72 -1
  86. package/dist/cli/index.cjs.map +1 -1
  87. package/dist/cli/index.d.cts.map +1 -1
  88. package/dist/cli/index.d.ts.map +1 -1
  89. package/dist/cli/index.js +73 -2
  90. package/dist/cli/index.js.map +1 -1
  91. package/dist/constants.cjs +13 -1
  92. package/dist/constants.cjs.map +1 -1
  93. package/dist/constants.d.cts +12 -0
  94. package/dist/constants.d.cts.map +1 -1
  95. package/dist/constants.d.ts +12 -0
  96. package/dist/constants.d.ts.map +1 -1
  97. package/dist/constants.js +12 -0
  98. package/dist/constants.js.map +1 -1
  99. package/dist/core/engine.cjs +4 -0
  100. package/dist/core/engine.cjs.map +1 -1
  101. package/dist/core/engine.d.cts.map +1 -1
  102. package/dist/core/engine.d.ts.map +1 -1
  103. package/dist/core/engine.js +4 -0
  104. package/dist/core/engine.js.map +1 -1
  105. package/dist/core/engines/tinybench-engine.cjs +163 -131
  106. package/dist/core/engines/tinybench-engine.cjs.map +1 -1
  107. package/dist/core/engines/tinybench-engine.d.cts +6 -0
  108. package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
  109. package/dist/core/engines/tinybench-engine.d.ts +6 -0
  110. package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
  111. package/dist/core/engines/tinybench-engine.js +163 -131
  112. package/dist/core/engines/tinybench-engine.js.map +1 -1
  113. package/dist/errors/base.cjs +2 -1
  114. package/dist/errors/base.cjs.map +1 -1
  115. package/dist/errors/base.d.cts.map +1 -1
  116. package/dist/errors/base.d.ts.map +1 -1
  117. package/dist/errors/base.js +2 -1
  118. package/dist/errors/base.js.map +1 -1
  119. package/dist/reporters/human.cjs +83 -27
  120. package/dist/reporters/human.cjs.map +1 -1
  121. package/dist/reporters/human.d.cts +1 -0
  122. package/dist/reporters/human.d.cts.map +1 -1
  123. package/dist/reporters/human.d.ts +1 -0
  124. package/dist/reporters/human.d.ts.map +1 -1
  125. package/dist/reporters/human.js +83 -27
  126. package/dist/reporters/human.js.map +1 -1
  127. package/dist/reporters/simple.cjs +68 -21
  128. package/dist/reporters/simple.cjs.map +1 -1
  129. package/dist/reporters/simple.d.cts +1 -0
  130. package/dist/reporters/simple.d.cts.map +1 -1
  131. package/dist/reporters/simple.d.ts +1 -0
  132. package/dist/reporters/simple.d.ts.map +1 -1
  133. package/dist/reporters/simple.js +68 -21
  134. package/dist/reporters/simple.js.map +1 -1
  135. package/dist/services/config-manager.cjs +1 -1
  136. package/dist/services/config-manager.cjs.map +1 -1
  137. package/dist/services/config-manager.js +2 -2
  138. package/dist/services/config-manager.js.map +1 -1
  139. package/package.json +60 -31
  140. package/src/adapters/ava-adapter.ts +553 -0
  141. package/src/adapters/ava-hooks.ts +65 -0
  142. package/src/adapters/ava-register.ts +15 -0
  143. package/src/adapters/mocha-adapter.ts +284 -0
  144. package/src/adapters/node-test-adapter.ts +391 -0
  145. package/src/adapters/node-test-hooks.ts +71 -0
  146. package/src/adapters/node-test-register.ts +5 -0
  147. package/src/adapters/types.ts +281 -0
  148. package/src/cli/commands/init.ts +25 -17
  149. package/src/cli/commands/run.ts +9 -2
  150. package/src/cli/commands/test.ts +546 -0
  151. package/src/cli/index.ts +81 -1
  152. package/src/constants.ts +15 -0
  153. package/src/core/engine.ts +5 -0
  154. package/src/core/engines/tinybench-engine.ts +213 -141
  155. package/src/errors/base.ts +3 -2
  156. package/src/reporters/human.ts +107 -36
  157. package/src/reporters/simple.ts +81 -22
  158. package/src/services/config-manager.ts +2 -2
@@ -0,0 +1,284 @@
1
+ /**
2
+ * ModestBench Mocha Adapter
3
+ *
4
+ * Captures test definitions from Mocha test files by replacing global
5
+ * `describe`, `it`, and hook functions before the test file loads.
6
+ *
7
+ * Mocha exposes these as globals, making interception straightforward - no ES
8
+ * module loader hooks required.
9
+ */
10
+
11
+ import { pathToFileURL } from 'node:url';
12
+
13
+ import type {
14
+ CapturedSuite,
15
+ CapturedTest,
16
+ CapturedTestFile,
17
+ SuiteHooks,
18
+ TestFrameworkAdapter,
19
+ } from './types.js';
20
+
21
+ /**
22
+ * Internal state for capturing test definitions
23
+ */
24
+ interface CaptureState {
25
+ /** Stack of suite contexts for nested describes */
26
+ currentSuite: CapturedSuite | null;
27
+ /** Root-level suites */
28
+ rootSuites: CapturedSuite[];
29
+ /** Root-level tests (if any, though Mocha usually uses describe) */
30
+ rootTests: CapturedTest[];
31
+ }
32
+
33
+ /**
34
+ * Mocha-style describe function signature
35
+ */
36
+ type DescribeFn = {
37
+ (name: string, fn: () => void): void;
38
+ only: (name: string, fn: () => void) => void;
39
+ skip: (name: string, fn: () => void) => void;
40
+ };
41
+
42
+ /**
43
+ * Mocha-style hook function signature
44
+ */
45
+ type HookFn = (fn: () => Promise<void> | void) => void;
46
+
47
+ /**
48
+ * Mocha-style it function signature
49
+ */
50
+ type ItFn = {
51
+ (name: string, fn: () => Promise<void> | void): void;
52
+ only: (name: string, fn: () => Promise<void> | void) => void;
53
+ skip: (name: string, fn: () => Promise<void> | void) => void;
54
+ };
55
+
56
+ /**
57
+ * Mocha test framework adapter
58
+ *
59
+ * Captures tests by installing global mocks before importing the test file.
60
+ */
61
+ export class MochaAdapter implements TestFrameworkAdapter {
62
+ readonly framework = 'mocha' as const;
63
+
64
+ /**
65
+ * Capture test definitions from a Mocha test file
66
+ *
67
+ * @param filePath - Absolute path to the test file
68
+ * @returns Captured test structure
69
+ */
70
+ async capture(filePath: string): Promise<CapturedTestFile> {
71
+ // Install our mock globals
72
+ const state = installMochaGlobals();
73
+
74
+ try {
75
+ // Import the test file - this triggers describe/it calls
76
+ // which populate our state
77
+ const fileUrl = pathToFileURL(filePath).href;
78
+
79
+ // Use a cache-busting query param to ensure fresh import
80
+ const bustCache = `?t=${Date.now()}`;
81
+ await import(fileUrl + bustCache);
82
+
83
+ // Return captured structure
84
+ return {
85
+ filePath,
86
+ framework: 'mocha',
87
+ rootSuites: state.rootSuites,
88
+ rootTests: state.rootTests,
89
+ };
90
+ } finally {
91
+ // Clean up globals
92
+ uninstallMochaGlobals();
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Create empty hooks structure
99
+ */
100
+ const createEmptyHooks = (): SuiteHooks => {
101
+ return {
102
+ after: [],
103
+ afterEach: [],
104
+ before: [],
105
+ beforeEach: [],
106
+ };
107
+ };
108
+
109
+ /**
110
+ * Create a new suite structure
111
+ */
112
+ const createSuite = (name: string): CapturedSuite => {
113
+ return {
114
+ children: [],
115
+ hooks: createEmptyHooks(),
116
+ name,
117
+ tests: [],
118
+ };
119
+ };
120
+
121
+ /**
122
+ * Install Mocha global mocks and return the capture state
123
+ *
124
+ * This replaces globalThis.describe, globalThis.it, etc. with our capturing
125
+ * implementations.
126
+ */
127
+ const installMochaGlobals = (): CaptureState => {
128
+ const state: CaptureState = {
129
+ currentSuite: null,
130
+ rootSuites: [],
131
+ rootTests: [],
132
+ };
133
+
134
+ // describe() - creates a suite
135
+ const describe: DescribeFn = (name: string, fn: () => void) => {
136
+ const suite = createSuite(name);
137
+ const parent = state.currentSuite;
138
+
139
+ if (parent) {
140
+ // Nested describe: inherit beforeEach/afterEach from parent
141
+ // and add to parent's children
142
+ parent.children.push(suite);
143
+ } else {
144
+ // Root-level describe
145
+ state.rootSuites.push(suite);
146
+ }
147
+
148
+ // Enter this suite context
149
+ state.currentSuite = suite;
150
+
151
+ // Execute the describe body to capture tests and nested describes
152
+ fn();
153
+
154
+ // Exit back to parent context
155
+ state.currentSuite = parent;
156
+ };
157
+
158
+ // describe.only() - marks suite as exclusive
159
+ describe.only = (name: string, fn: () => void) => {
160
+ // For benchmarking, we treat .only the same as regular
161
+ // (filtering happens at a higher level)
162
+ describe(name, fn);
163
+ };
164
+
165
+ // describe.skip() - marks suite as skipped
166
+ describe.skip = (_name: string, _fn: () => void) => {
167
+ // Skip entirely - don't even register
168
+ };
169
+
170
+ // it() - creates a test
171
+ const it: ItFn = (name: string, fn: () => Promise<void> | void) => {
172
+ const test: CapturedTest = { fn, name };
173
+
174
+ if (state.currentSuite) {
175
+ state.currentSuite.tests.push(test);
176
+ } else {
177
+ // Root-level test (unusual for Mocha but supported)
178
+ state.rootTests.push(test);
179
+ }
180
+ };
181
+
182
+ // it.only() - marks test as exclusive
183
+ it.only = (name: string, fn: () => Promise<void> | void) => {
184
+ const test: CapturedTest = { fn, name, only: true };
185
+
186
+ if (state.currentSuite) {
187
+ state.currentSuite.tests.push(test);
188
+ } else {
189
+ state.rootTests.push(test);
190
+ }
191
+ };
192
+
193
+ // it.skip() - marks test as skipped
194
+ it.skip = (name: string, fn: () => Promise<void> | void) => {
195
+ const test: CapturedTest = { fn, name, skip: true };
196
+
197
+ if (state.currentSuite) {
198
+ state.currentSuite.tests.push(test);
199
+ } else {
200
+ state.rootTests.push(test);
201
+ }
202
+ };
203
+
204
+ // before() - runs once before all tests in suite
205
+ const before: HookFn = (fn: () => Promise<void> | void) => {
206
+ if (state.currentSuite) {
207
+ (
208
+ state.currentSuite.hooks.before as Array<() => Promise<void> | void>
209
+ ).push(fn);
210
+ }
211
+ // Root-level before is ignored (no suite to attach to)
212
+ };
213
+
214
+ // after() - runs once after all tests in suite
215
+ const after: HookFn = (fn: () => Promise<void> | void) => {
216
+ if (state.currentSuite) {
217
+ (
218
+ state.currentSuite.hooks.after as Array<() => Promise<void> | void>
219
+ ).push(fn);
220
+ }
221
+ };
222
+
223
+ // beforeEach() - runs before each test
224
+ const beforeEach: HookFn = (fn: () => Promise<void> | void) => {
225
+ if (state.currentSuite) {
226
+ (
227
+ state.currentSuite.hooks.beforeEach as Array<() => Promise<void> | void>
228
+ ).push(fn);
229
+ }
230
+ };
231
+
232
+ // afterEach() - runs after each test
233
+ const afterEach: HookFn = (fn: () => Promise<void> | void) => {
234
+ if (state.currentSuite) {
235
+ (
236
+ state.currentSuite.hooks.afterEach as Array<() => Promise<void> | void>
237
+ ).push(fn);
238
+ }
239
+ };
240
+
241
+ // Install globals
242
+ // @ts-expect-error - intentionally modifying globalThis
243
+ globalThis.describe = describe;
244
+ // @ts-expect-error - intentionally modifying globalThis
245
+ globalThis.it = it;
246
+ // @ts-expect-error - intentionally modifying globalThis
247
+ globalThis.before = before;
248
+ // @ts-expect-error - intentionally modifying globalThis
249
+ globalThis.after = after;
250
+ // @ts-expect-error - intentionally modifying globalThis
251
+ globalThis.beforeEach = beforeEach;
252
+ // @ts-expect-error - intentionally modifying globalThis
253
+ globalThis.afterEach = afterEach;
254
+
255
+ // Mocha aliases
256
+ // @ts-expect-error - intentionally modifying globalThis
257
+ globalThis.context = describe;
258
+ // @ts-expect-error - intentionally modifying globalThis
259
+ globalThis.specify = it;
260
+
261
+ return state;
262
+ };
263
+
264
+ /**
265
+ * Remove Mocha globals installed by installMochaGlobals
266
+ */
267
+ const uninstallMochaGlobals = (): void => {
268
+ // @ts-expect-error - cleaning up globalThis
269
+ delete globalThis.describe;
270
+ // @ts-expect-error - cleaning up globalThis
271
+ delete globalThis.it;
272
+ // @ts-expect-error - cleaning up globalThis
273
+ delete globalThis.before;
274
+ // @ts-expect-error - cleaning up globalThis
275
+ delete globalThis.after;
276
+ // @ts-expect-error - cleaning up globalThis
277
+ delete globalThis.beforeEach;
278
+ // @ts-expect-error - cleaning up globalThis
279
+ delete globalThis.afterEach;
280
+ // @ts-expect-error - cleaning up globalThis
281
+ delete globalThis.context;
282
+ // @ts-expect-error - cleaning up globalThis
283
+ delete globalThis.specify;
284
+ };
@@ -0,0 +1,391 @@
1
+ /**
2
+ * ModestBench Node.js Test Runner Adapter
3
+ *
4
+ * Captures test definitions from node:test files using ES module loader hooks.
5
+ *
6
+ * Unlike Mocha (which uses globals), node:test requires ES module import
7
+ * interception. We use Node.js module.register() API to install a custom loader
8
+ * that returns our capturing mock instead of the real node:test.
9
+ *
10
+ * Architecture:
11
+ *
12
+ * 1. Register a custom loader that intercepts node:test imports
13
+ * 2. Import the test file - this triggers test/describe calls
14
+ * 3. Retrieve captured state from global storage
15
+ * 4. Unregister the loader
16
+ */
17
+
18
+ import { pathToFileURL } from 'node:url';
19
+
20
+ import type {
21
+ CapturedSuite,
22
+ CapturedTest,
23
+ CapturedTestFile,
24
+ SuiteHooks,
25
+ TestFrameworkAdapter,
26
+ } from './types.js';
27
+
28
+ /**
29
+ * Global capture state key
30
+ */
31
+ const CAPTURE_STATE_KEY = '__MODESTBENCH_NODE_TEST_CAPTURE__';
32
+
33
+ /**
34
+ * Internal capture state structure
35
+ */
36
+ interface CaptureState {
37
+ currentSuite: MutableCapturedSuite | null;
38
+ rootSuites: MutableCapturedSuite[];
39
+ rootTests: MutableCapturedTest[];
40
+ }
41
+
42
+ /**
43
+ * Mutable version of CapturedSuite for building state
44
+ */
45
+ interface MutableCapturedSuite {
46
+ children: MutableCapturedSuite[];
47
+ hooks: MutableSuiteHooks;
48
+ name: string;
49
+ tests: MutableCapturedTest[];
50
+ }
51
+
52
+ /**
53
+ * Mutable version of CapturedTest
54
+ */
55
+ interface MutableCapturedTest {
56
+ fn: () => Promise<void> | void;
57
+ name: string;
58
+ only?: boolean;
59
+ skip?: boolean;
60
+ }
61
+
62
+ /**
63
+ * Mutable version of SuiteHooks
64
+ */
65
+ interface MutableSuiteHooks {
66
+ after: Array<() => Promise<void> | void>;
67
+ afterEach: Array<() => Promise<void> | void>;
68
+ before: Array<() => Promise<void> | void>;
69
+ beforeEach: Array<() => Promise<void> | void>;
70
+ }
71
+
72
+ /**
73
+ * Node.js test runner adapter
74
+ *
75
+ * Captures test definitions by installing mock implementations before importing
76
+ * the test file.
77
+ *
78
+ * IMPORTANT: For this to work with actual node:test imports, you must run
79
+ * Node.js with our loader: node --import modestbench/node-test your-test.js
80
+ *
81
+ * Without the loader, this adapter only works with test files that use
82
+ * globalThis.**MODESTBENCH_NODE_TEST_MOCK** directly (not useful for real test
83
+ * files).
84
+ */
85
+ export class NodeTestAdapter implements TestFrameworkAdapter {
86
+ readonly framework = 'node-test' as const;
87
+
88
+ /**
89
+ * Capture test definitions from a node:test file
90
+ *
91
+ * @param filePath - Absolute path to the test file
92
+ * @returns Captured test structure
93
+ */
94
+ async capture(filePath: string): Promise<CapturedTestFile> {
95
+ // Initialize capture state
96
+ const state = initCaptureState();
97
+
98
+ // Install mocks
99
+ installNodeTestMocks(state);
100
+
101
+ try {
102
+ // Import the test file
103
+ // The loader hook will intercept 'node:test' and use our mocks
104
+ const fileUrl = pathToFileURL(filePath).href;
105
+ const bustCache = `?t=${Date.now()}`;
106
+ await import(fileUrl + bustCache);
107
+
108
+ // Return captured structure
109
+ return toCapturedTestFile(state, filePath);
110
+ } finally {
111
+ // Clean up
112
+ uninstallNodeTestMocks();
113
+ clearCaptureState();
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Clear capture state from globalThis
120
+ */
121
+ const clearCaptureState = (): void => {
122
+ // @ts-expect-error - intentionally using globalThis
123
+ delete globalThis[CAPTURE_STATE_KEY];
124
+ };
125
+
126
+ /**
127
+ * Create empty hooks structure
128
+ */
129
+ const createEmptyHooks = (): MutableSuiteHooks => {
130
+ return {
131
+ after: [],
132
+ afterEach: [],
133
+ before: [],
134
+ beforeEach: [],
135
+ };
136
+ };
137
+
138
+ /**
139
+ * Create a new suite structure
140
+ */
141
+ const createSuite = (name: string): MutableCapturedSuite => {
142
+ return {
143
+ children: [],
144
+ hooks: createEmptyHooks(),
145
+ name,
146
+ tests: [],
147
+ };
148
+ };
149
+
150
+ /**
151
+ * Initialize capture state on globalThis
152
+ */
153
+ const initCaptureState = (): CaptureState => {
154
+ const state: CaptureState = {
155
+ currentSuite: null,
156
+ rootSuites: [],
157
+ rootTests: [],
158
+ };
159
+
160
+ // @ts-expect-error - intentionally using globalThis for cross-module state
161
+ globalThis[CAPTURE_STATE_KEY] = state;
162
+
163
+ return state;
164
+ };
165
+
166
+ /**
167
+ * Install node:test mocks on globalThis for module interception
168
+ *
169
+ * Since we can't truly intercept ES imports without a loader, we use a
170
+ * workaround: install mocks that test files can import via a special path.
171
+ *
172
+ * For true node:test interception, users must run with: node --import
173
+ * modestbench/register test-file.js
174
+ */
175
+ const installNodeTestMocks = (state: CaptureState): void => {
176
+ // Create mock test function
177
+ const mockTest = Object.assign(
178
+ (
179
+ nameOrOptions: string | { name?: string; only?: boolean; skip?: boolean },
180
+ fnOrOptions?:
181
+ | (() => Promise<void> | void)
182
+ | { only?: boolean; skip?: boolean },
183
+ maybeFn?: () => Promise<void> | void,
184
+ ): Promise<void> => {
185
+ let name: string;
186
+ let fn: () => Promise<void> | void;
187
+ let skip = false;
188
+ let only = false;
189
+
190
+ // Parse arguments - node:test has flexible signatures
191
+ if (typeof nameOrOptions === 'string') {
192
+ name = nameOrOptions;
193
+ if (typeof fnOrOptions === 'function') {
194
+ fn = fnOrOptions;
195
+ } else if (fnOrOptions && typeof fnOrOptions === 'object') {
196
+ skip = fnOrOptions.skip ?? false;
197
+ only = fnOrOptions.only ?? false;
198
+ fn = maybeFn ?? (() => {});
199
+ } else {
200
+ fn = () => {};
201
+ }
202
+ } else {
203
+ name = nameOrOptions.name ?? 'unnamed test';
204
+ skip = nameOrOptions.skip ?? false;
205
+ only = nameOrOptions.only ?? false;
206
+ fn = (fnOrOptions as () => Promise<void> | void) ?? (() => {});
207
+ }
208
+
209
+ const test: MutableCapturedTest = { fn, name, only, skip };
210
+
211
+ if (state.currentSuite) {
212
+ state.currentSuite.tests.push(test);
213
+ } else {
214
+ state.rootTests.push(test);
215
+ }
216
+
217
+ return Promise.resolve();
218
+ },
219
+ {
220
+ only: function only(
221
+ name: string,
222
+ fn?: () => Promise<void> | void,
223
+ ): Promise<void> {
224
+ return mockTest(name, { only: true }, fn);
225
+ },
226
+ skip: function skip(
227
+ name: string,
228
+ fn?: () => Promise<void> | void,
229
+ ): Promise<void> {
230
+ return mockTest(name, { skip: true }, fn);
231
+ },
232
+ todo: function todo(
233
+ name: string,
234
+ fn?: () => Promise<void> | void,
235
+ ): Promise<void> {
236
+ return mockTest(name, { skip: true }, fn);
237
+ },
238
+ },
239
+ );
240
+
241
+ // Create mock describe function
242
+ const mockDescribe = Object.assign(
243
+ (
244
+ nameOrOptions: string | { name?: string; only?: boolean; skip?: boolean },
245
+ fnOrOptions?: (() => void) | { only?: boolean; skip?: boolean },
246
+ maybeFn?: () => void,
247
+ ): Promise<void> => {
248
+ let name: string;
249
+ let fn: (() => void) | undefined;
250
+
251
+ if (typeof nameOrOptions === 'string') {
252
+ name = nameOrOptions;
253
+ if (typeof fnOrOptions === 'function') {
254
+ fn = fnOrOptions;
255
+ } else {
256
+ fn = maybeFn;
257
+ }
258
+ } else {
259
+ name = nameOrOptions.name ?? 'unnamed suite';
260
+ fn = fnOrOptions as (() => void) | undefined;
261
+ }
262
+
263
+ const suite = createSuite(name);
264
+ const parent = state.currentSuite;
265
+
266
+ if (parent) {
267
+ parent.children.push(suite);
268
+ } else {
269
+ state.rootSuites.push(suite);
270
+ }
271
+
272
+ // Enter suite context
273
+ state.currentSuite = suite;
274
+
275
+ // Execute describe body
276
+ if (fn) {
277
+ fn();
278
+ }
279
+
280
+ // Exit back to parent
281
+ state.currentSuite = parent;
282
+
283
+ return Promise.resolve();
284
+ },
285
+ {
286
+ only: function only(name: string, fn?: () => void): Promise<void> {
287
+ return mockDescribe(name, fn);
288
+ },
289
+ skip: function skip(_name: string, _fn?: () => void): Promise<void> {
290
+ return Promise.resolve();
291
+ },
292
+ todo: function todo(_name: string, _fn?: () => void): Promise<void> {
293
+ return Promise.resolve();
294
+ },
295
+ },
296
+ );
297
+
298
+ // Hook functions
299
+ const mockBefore = (fn: () => Promise<void> | void): void => {
300
+ if (state.currentSuite) {
301
+ state.currentSuite.hooks.before.push(fn);
302
+ }
303
+ };
304
+
305
+ const mockAfter = (fn: () => Promise<void> | void): void => {
306
+ if (state.currentSuite) {
307
+ state.currentSuite.hooks.after.push(fn);
308
+ }
309
+ };
310
+
311
+ const mockBeforeEach = (fn: () => Promise<void> | void): void => {
312
+ if (state.currentSuite) {
313
+ state.currentSuite.hooks.beforeEach.push(fn);
314
+ }
315
+ };
316
+
317
+ const mockAfterEach = (fn: () => Promise<void> | void): void => {
318
+ if (state.currentSuite) {
319
+ state.currentSuite.hooks.afterEach.push(fn);
320
+ }
321
+ };
322
+
323
+ // Install on globalThis for the loader to access
324
+ // @ts-expect-error - intentionally using globalThis
325
+ globalThis.__MODESTBENCH_NODE_TEST_MOCK__ = {
326
+ after: mockAfter,
327
+ afterEach: mockAfterEach,
328
+ before: mockBefore,
329
+ beforeEach: mockBeforeEach,
330
+ default: mockTest,
331
+ describe: mockDescribe,
332
+ it: mockTest,
333
+ mock: {
334
+ fn: () => () => {},
335
+ getter: () => {},
336
+ method: () => {},
337
+ reset: () => {},
338
+ restoreAll: () => {},
339
+ setter: () => {},
340
+ timers: {
341
+ enable: () => {},
342
+ reset: () => {},
343
+ runAll: () => {},
344
+ tick: () => {},
345
+ },
346
+ },
347
+ suite: mockDescribe,
348
+ test: mockTest,
349
+ };
350
+ };
351
+
352
+ /**
353
+ * Convert mutable capture state to immutable CapturedTestFile
354
+ */
355
+ const toCapturedTestFile = (
356
+ state: CaptureState,
357
+ filePath: string,
358
+ ): CapturedTestFile => {
359
+ return {
360
+ filePath,
361
+ framework: 'node-test',
362
+ rootSuites: state.rootSuites.map(toImmutableSuite),
363
+ rootTests: state.rootTests.map(toImmutableTest),
364
+ };
365
+ };
366
+
367
+ const toImmutableSuite = (suite: MutableCapturedSuite): CapturedSuite => {
368
+ return {
369
+ children: suite.children.map(toImmutableSuite),
370
+ hooks: suite.hooks as SuiteHooks,
371
+ name: suite.name,
372
+ tests: suite.tests.map(toImmutableTest),
373
+ };
374
+ };
375
+
376
+ const toImmutableTest = (test: MutableCapturedTest): CapturedTest => {
377
+ return {
378
+ fn: test.fn,
379
+ name: test.name,
380
+ only: test.only,
381
+ skip: test.skip,
382
+ };
383
+ };
384
+
385
+ /**
386
+ * Uninstall node:test mocks
387
+ */
388
+ const uninstallNodeTestMocks = (): void => {
389
+ // @ts-expect-error - cleaning up globalThis
390
+ delete globalThis.__MODESTBENCH_NODE_TEST_MOCK__;
391
+ };