modestbench 0.5.1 → 0.7.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 (117) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +6 -2
  3. package/dist/adapters/jest-adapter.cjs +496 -0
  4. package/dist/adapters/jest-adapter.cjs.map +1 -0
  5. package/dist/adapters/jest-adapter.d.cts +42 -0
  6. package/dist/adapters/jest-adapter.d.cts.map +1 -0
  7. package/dist/adapters/jest-adapter.d.ts +42 -0
  8. package/dist/adapters/jest-adapter.d.ts.map +1 -0
  9. package/dist/adapters/jest-adapter.js +459 -0
  10. package/dist/adapters/jest-adapter.js.map +1 -0
  11. package/dist/adapters/jest-hooks.cjs +83 -0
  12. package/dist/adapters/jest-hooks.cjs.map +1 -0
  13. package/dist/adapters/jest-hooks.d.cts +24 -0
  14. package/dist/adapters/jest-hooks.d.cts.map +1 -0
  15. package/dist/adapters/jest-hooks.d.ts +24 -0
  16. package/dist/adapters/jest-hooks.d.ts.map +1 -0
  17. package/dist/adapters/jest-hooks.js +78 -0
  18. package/dist/adapters/jest-hooks.js.map +1 -0
  19. package/dist/adapters/jest-register.cjs +17 -0
  20. package/dist/adapters/jest-register.cjs.map +1 -0
  21. package/dist/adapters/jest-register.d.cts +12 -0
  22. package/dist/adapters/jest-register.d.cts.map +1 -0
  23. package/dist/adapters/jest-register.d.ts +12 -0
  24. package/dist/adapters/jest-register.d.ts.map +1 -0
  25. package/dist/adapters/jest-register.js +15 -0
  26. package/dist/adapters/jest-register.js.map +1 -0
  27. package/dist/adapters/types.cjs.map +1 -1
  28. package/dist/adapters/types.d.cts +5 -1
  29. package/dist/adapters/types.d.cts.map +1 -1
  30. package/dist/adapters/types.d.ts +5 -1
  31. package/dist/adapters/types.d.ts.map +1 -1
  32. package/dist/adapters/types.js.map +1 -1
  33. package/dist/cli/commands/run.cjs +17 -11
  34. package/dist/cli/commands/run.cjs.map +1 -1
  35. package/dist/cli/commands/run.js +9 -3
  36. package/dist/cli/commands/run.js.map +1 -1
  37. package/dist/cli/commands/test.cjs +17 -15
  38. package/dist/cli/commands/test.cjs.map +1 -1
  39. package/dist/cli/commands/test.d.cts.map +1 -1
  40. package/dist/cli/commands/test.d.ts.map +1 -1
  41. package/dist/cli/commands/test.js +5 -3
  42. package/dist/cli/commands/test.js.map +1 -1
  43. package/dist/cli/index.cjs +5 -2
  44. package/dist/cli/index.cjs.map +1 -1
  45. package/dist/cli/index.d.cts.map +1 -1
  46. package/dist/cli/index.d.ts.map +1 -1
  47. package/dist/cli/index.js +6 -3
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/constants.cjs +1 -0
  50. package/dist/constants.cjs.map +1 -1
  51. package/dist/constants.d.cts +1 -0
  52. package/dist/constants.d.cts.map +1 -1
  53. package/dist/constants.d.ts +1 -0
  54. package/dist/constants.d.ts.map +1 -1
  55. package/dist/constants.js +1 -0
  56. package/dist/constants.js.map +1 -1
  57. package/dist/index.cjs +4 -1
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.d.cts +1 -0
  60. package/dist/index.d.cts.map +1 -1
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +2 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/reporters/index.cjs +3 -1
  66. package/dist/reporters/index.cjs.map +1 -1
  67. package/dist/reporters/index.d.cts +1 -0
  68. package/dist/reporters/index.d.cts.map +1 -1
  69. package/dist/reporters/index.d.ts +1 -0
  70. package/dist/reporters/index.d.ts.map +1 -1
  71. package/dist/reporters/index.js +1 -0
  72. package/dist/reporters/index.js.map +1 -1
  73. package/dist/reporters/nyan.cjs +318 -0
  74. package/dist/reporters/nyan.cjs.map +1 -0
  75. package/dist/reporters/nyan.d.cts +118 -0
  76. package/dist/reporters/nyan.d.cts.map +1 -0
  77. package/dist/reporters/nyan.d.ts +118 -0
  78. package/dist/reporters/nyan.d.ts.map +1 -0
  79. package/dist/reporters/nyan.js +314 -0
  80. package/dist/reporters/nyan.js.map +1 -0
  81. package/dist/types/core.cjs.map +1 -1
  82. package/dist/types/core.d.cts +13 -12
  83. package/dist/types/core.d.cts.map +1 -1
  84. package/dist/types/core.d.ts +13 -12
  85. package/dist/types/core.d.ts.map +1 -1
  86. package/dist/types/core.js.map +1 -1
  87. package/dist/types/index.cjs +0 -2
  88. package/dist/types/index.cjs.map +1 -1
  89. package/dist/types/index.d.cts +0 -1
  90. package/dist/types/index.d.cts.map +1 -1
  91. package/dist/types/index.d.ts +0 -1
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/index.js +0 -2
  94. package/dist/types/index.js.map +1 -1
  95. package/package.json +28 -8
  96. package/src/adapters/jest-adapter.ts +563 -0
  97. package/src/adapters/jest-hooks.ts +82 -0
  98. package/src/adapters/jest-register.ts +16 -0
  99. package/src/adapters/types.ts +5 -1
  100. package/src/cli/commands/run.ts +10 -3
  101. package/src/cli/commands/test.ts +5 -3
  102. package/src/cli/index.ts +10 -2
  103. package/src/constants.ts +2 -1
  104. package/src/index.ts +3 -0
  105. package/src/reporters/index.ts +1 -0
  106. package/src/reporters/nyan.ts +409 -0
  107. package/src/types/core.ts +16 -14
  108. package/src/types/index.ts +0 -3
  109. package/dist/types/cli.cjs +0 -12
  110. package/dist/types/cli.cjs.map +0 -1
  111. package/dist/types/cli.d.cts +0 -75
  112. package/dist/types/cli.d.cts.map +0 -1
  113. package/dist/types/cli.d.ts +0 -75
  114. package/dist/types/cli.d.ts.map +0 -1
  115. package/dist/types/cli.js +0 -9
  116. package/dist/types/cli.js.map +0 -1
  117. package/src/types/cli.ts +0 -82
@@ -4,7 +4,6 @@
4
4
  * Main entry point for all TypeScript type definitions used in ModestBench.
5
5
  * This file re-exports all types from the individual type modules.
6
6
  */
7
- export * from "./cli.cjs";
8
7
  export type * from "./core.cjs";
9
8
  export { createRunId, createTaskId } from "./core.cjs";
10
9
  export type * from "./interfaces.cjs";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,0BAAyB;AAGzB,gCAA+B;AAE/B,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,mBAAkB;AAGtD,sCAAqC;AAGrC,oCAAmC;AAGnC,8BAA6B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,gCAA+B;AAE/B,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,mBAAkB;AAGtD,sCAAqC;AAGrC,oCAAmC;AAGnC,8BAA6B"}
@@ -4,7 +4,6 @@
4
4
  * Main entry point for all TypeScript type definitions used in ModestBench.
5
5
  * This file re-exports all types from the individual type modules.
6
6
  */
7
- export * from "./cli.js";
8
7
  export type * from "./core.js";
9
8
  export { createRunId, createTaskId } from "./core.js";
10
9
  export type * from "./interfaces.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,yBAAyB;AAGzB,+BAA+B;AAE/B,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB;AAGtD,qCAAqC;AAGrC,mCAAmC;AAGnC,6BAA6B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,+BAA+B;AAE/B,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB;AAGtD,qCAAqC;AAGrC,mCAAmC;AAGnC,6BAA6B"}
@@ -4,8 +4,6 @@
4
4
  * Main entry point for all TypeScript type definitions used in ModestBench.
5
5
  * This file re-exports all types from the individual type modules.
6
6
  */
7
- // CLI-specific types
8
- export * from "./cli.js";
9
7
  // Helper functions from core (value exports)
10
8
  export { createRunId, createTaskId } from "./core.js";
11
9
  // Utility types and helpers
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qBAAqB;AACrB,yBAAyB;AAIzB,6CAA6C;AAC7C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB;AAQtD,4BAA4B;AAC5B,6BAA6B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,6CAA6C;AAC7C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB;AAQtD,4BAA4B;AAC5B,6BAA6B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modestbench",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "A full-ass benchmarking framework for Node.js",
6
6
  "repository": {
@@ -41,6 +41,16 @@
41
41
  "import": "./dist/cli/index.js",
42
42
  "require": "./dist/cli/index.cjs"
43
43
  },
44
+ "./jest": {
45
+ "types": "./dist/adapters/jest-register.d.cts",
46
+ "import": "./dist/adapters/jest-register.js",
47
+ "require": "./dist/adapters/jest-register.cjs"
48
+ },
49
+ "./jest-hooks": {
50
+ "types": "./dist/adapters/jest-hooks.d.cts",
51
+ "import": "./dist/adapters/jest-hooks.js",
52
+ "require": "./dist/adapters/jest-hooks.cjs"
53
+ },
44
54
  "./node-test": {
45
55
  "types": "./dist/adapters/node-test-register.d.cts",
46
56
  "import": "./dist/adapters/node-test-register.js",
@@ -122,19 +132,22 @@
122
132
  "zod": "4.1.13"
123
133
  },
124
134
  "devDependencies": {
125
- "@astrojs/mdx": "4.3.12",
126
- "@astrojs/starlight": "0.37.0",
135
+ "@astrojs/mdx": "4.3.13",
136
+ "@astrojs/starlight": "0.37.1",
127
137
  "@commitlint/cli": "20.2.0",
128
138
  "@commitlint/config-conventional": "20.2.0",
129
139
  "@eslint/js": "9.39.1",
140
+ "@jest/globals": "30.2.0",
130
141
  "@pasqal-io/starlight-client-mermaid": "0.1.0",
131
142
  "@stylistic/eslint-plugin": "5.6.1",
132
- "@types/node": "24.10.1",
143
+ "@types/mocha": "10.0.10",
144
+ "@types/node": "24.10.3",
133
145
  "@types/wallabyjs": "0.0.15",
134
146
  "@types/yargs": "17.0.35",
135
147
  "asciinema-player": "3.12.1",
136
- "astro": "5.16.4",
148
+ "astro": "5.16.5",
137
149
  "astro-broken-link-checker": "file:./vendor/astro-broken-link-checker",
150
+ "ava": "6.4.1",
138
151
  "bupkis": "0.14.0",
139
152
  "cspell": "9.4.0",
140
153
  "eslint": "9.39.1",
@@ -142,10 +155,12 @@
142
155
  "eslint-plugin-perfectionist": "4.15.1",
143
156
  "globals": "16.5.0",
144
157
  "husky": "9.1.7",
145
- "knip": "5.71.0",
158
+ "jest": "30.2.0",
159
+ "knip": "5.73.1",
146
160
  "lint-staged": "16.2.7",
147
161
  "markdownlint-cli2": "0.20.0",
148
162
  "markdownlint-cli2-formatter-pretty": "0.0.9",
163
+ "mocha": "11.7.5",
149
164
  "npm-run-all2": "8.0.4",
150
165
  "prettier": "3.7.4",
151
166
  "prettier-plugin-jsdoc": "1.8.0",
@@ -154,7 +169,7 @@
154
169
  "tsd": "0.33.0",
155
170
  "tsx": "4.21.0",
156
171
  "typescript": "5.9.3",
157
- "typescript-eslint": "8.48.1",
172
+ "typescript-eslint": "8.49.0",
158
173
  "zshy": "0.6.0"
159
174
  },
160
175
  "publishConfig": {
@@ -164,10 +179,13 @@
164
179
  "knip": {
165
180
  "ignoreDependencies": [
166
181
  "@types/wallabyjs",
182
+ "@types/mocha",
167
183
  "markdownlint-cli2-formatter-pretty",
168
184
  "asciinema-player",
169
185
  "@astrojs/mdx",
170
- "astro-broken-link-checker"
186
+ "astro-broken-link-checker",
187
+ "jest",
188
+ "mocha"
171
189
  ],
172
190
  "ignore": [
173
191
  ".wallaby.js",
@@ -235,6 +253,8 @@
235
253
  "./ava": "./src/adapters/ava-register.ts",
236
254
  "./ava-hooks": "./src/adapters/ava-hooks.ts",
237
255
  "./cli": "./src/cli/index.ts",
256
+ "./jest": "./src/adapters/jest-register.ts",
257
+ "./jest-hooks": "./src/adapters/jest-hooks.ts",
238
258
  "./node-test": "./src/adapters/node-test-register.ts",
239
259
  "./node-test-hooks": "./src/adapters/node-test-hooks.ts",
240
260
  "./package.json": "./package.json"
@@ -0,0 +1,563 @@
1
+ /**
2
+ * ModestBench Jest Adapter
3
+ *
4
+ * Captures test definitions from Jest test files using ES module loader hooks.
5
+ *
6
+ * Jest differs from AVA and node:test in important ways:
7
+ *
8
+ * - Nested describe blocks: describe() callbacks execute IMMEDIATELY during file
9
+ * load to discover nested tests. We must run describe callbacks but NOT test
10
+ * functions.
11
+ * - Rich API surface: .skip, .only, .each, .todo, .concurrent, .failing modifiers
12
+ * - Jest object: Tests may access jest.fn(), jest.mock(), etc.
13
+ * - Expect assertions: Tests use expect() for assertions
14
+ *
15
+ * Architecture:
16
+ *
17
+ * 1. Install mock on globalThis
18
+ * 2. Loader intercepts '@jest/globals' imports and returns the mock
19
+ * 3. Import test file - describe callbacks execute, test functions are captured
20
+ * 4. Return captured test structure
21
+ */
22
+
23
+ import { pathToFileURL } from 'node:url';
24
+
25
+ import type {
26
+ CapturedSuite,
27
+ CapturedTest,
28
+ CapturedTestFile,
29
+ SuiteHooks,
30
+ TestFrameworkAdapter,
31
+ } from './types.js';
32
+
33
+ /**
34
+ * Global capture state key
35
+ */
36
+ const CAPTURE_STATE_KEY = '__MODESTBENCH_JEST_CAPTURE__';
37
+
38
+ /**
39
+ * Internal capture state structure
40
+ *
41
+ * Uses a stack to track nested describe blocks
42
+ */
43
+ interface CaptureState {
44
+ rootSuite: MutableCapturedSuite;
45
+ suiteStack: MutableCapturedSuite[];
46
+ }
47
+
48
+ /**
49
+ * Jest's each table type for parameterized tests
50
+ */
51
+ type EachTable = ReadonlyArray<readonly unknown[]> | readonly unknown[];
52
+
53
+ /**
54
+ * Jest's test function type
55
+ */
56
+ type JestTestFn = () => Promise<void> | void;
57
+
58
+ /**
59
+ * Internal captured suite with Jest-specific data
60
+ */
61
+ interface MutableCapturedSuite {
62
+ children: MutableCapturedSuite[];
63
+ hooks: {
64
+ afterAll: Array<{ fn: JestTestFn; timeout?: number }>;
65
+ afterEach: Array<{ fn: JestTestFn; timeout?: number }>;
66
+ beforeAll: Array<{ fn: JestTestFn; timeout?: number }>;
67
+ beforeEach: Array<{ fn: JestTestFn; timeout?: number }>;
68
+ };
69
+ name: string;
70
+ only?: boolean;
71
+ skip?: boolean;
72
+ tests: MutableCapturedTest[];
73
+ }
74
+
75
+ /**
76
+ * Internal captured test with Jest-specific flags
77
+ */
78
+ interface MutableCapturedTest {
79
+ fn: JestTestFn;
80
+ name: string;
81
+ only?: boolean;
82
+ skip?: boolean;
83
+ timeout?: number;
84
+ }
85
+
86
+ /**
87
+ * Get the current suite from the stack
88
+ */
89
+ const getCurrentSuite = (state: CaptureState): MutableCapturedSuite => {
90
+ return state.suiteStack[state.suiteStack.length - 1] ?? state.rootSuite;
91
+ };
92
+
93
+ /**
94
+ * Jest test framework adapter
95
+ *
96
+ * Captures test definitions by installing mock implementations before importing
97
+ * the test file.
98
+ *
99
+ * IMPORTANT: For this to work with Jest imports, you must run Node.js with our
100
+ * loader: node --import modestbench/jest your-test.js
101
+ */
102
+ export class JestAdapter implements TestFrameworkAdapter {
103
+ readonly framework = 'jest' as const;
104
+
105
+ /**
106
+ * Capture test definitions from a Jest test file
107
+ *
108
+ * @param filePath - Absolute path to the test file
109
+ * @returns Captured test structure
110
+ */
111
+ async capture(filePath: string): Promise<CapturedTestFile> {
112
+ // Initialize capture state
113
+ const state = initCaptureState();
114
+
115
+ // Install mocks
116
+ installJestMocks(state);
117
+
118
+ try {
119
+ // Import the test file
120
+ // The loader hook will intercept '@jest/globals' and use our mocks
121
+ // Describe callbacks will execute immediately, capturing the test structure
122
+ const fileUrl = pathToFileURL(filePath).href;
123
+ const bustCache = `?t=${Date.now()}`;
124
+ await import(fileUrl + bustCache);
125
+
126
+ // Return captured structure
127
+ return toCapturedTestFile(state, filePath);
128
+ } finally {
129
+ // Clean up
130
+ uninstallJestMocks();
131
+ clearCaptureState();
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Clear capture state from globalThis
138
+ */
139
+ const clearCaptureState = (): void => {
140
+ // @ts-expect-error - intentionally using globalThis
141
+ delete globalThis[CAPTURE_STATE_KEY];
142
+ };
143
+
144
+ /**
145
+ * Create a mock expect function
146
+ *
147
+ * Jest's expect() returns an object with assertion methods. We provide a stub
148
+ * that allows tests to call expect() without errors during structure capture.
149
+ * Since we capture but don't execute test functions, this is mainly for
150
+ * safety.
151
+ */
152
+ const createMockExpect = (): ((value: unknown) => unknown) => {
153
+ const noop = () => mockExpect;
154
+ const noopAsync = () => Promise.resolve(mockExpect);
155
+
156
+ // Chainable assertion mock
157
+ const assertionChain: Record<string, unknown> = {
158
+ not: {} as Record<string, unknown>,
159
+ rejects: {} as Record<string, unknown>,
160
+ resolves: {} as Record<string, unknown>,
161
+ };
162
+
163
+ // Common matchers - all return the chain for further chaining
164
+ const matchers = [
165
+ 'toBe',
166
+ 'toEqual',
167
+ 'toStrictEqual',
168
+ 'toBeNull',
169
+ 'toBeUndefined',
170
+ 'toBeDefined',
171
+ 'toBeTruthy',
172
+ 'toBeFalsy',
173
+ 'toBeNaN',
174
+ 'toContain',
175
+ 'toContainEqual',
176
+ 'toHaveLength',
177
+ 'toHaveProperty',
178
+ 'toMatch',
179
+ 'toMatchObject',
180
+ 'toMatchSnapshot',
181
+ 'toMatchInlineSnapshot',
182
+ 'toThrow',
183
+ 'toThrowError',
184
+ 'toThrowErrorMatchingSnapshot',
185
+ 'toThrowErrorMatchingInlineSnapshot',
186
+ 'toBeGreaterThan',
187
+ 'toBeGreaterThanOrEqual',
188
+ 'toBeLessThan',
189
+ 'toBeLessThanOrEqual',
190
+ 'toBeCloseTo',
191
+ 'toBeInstanceOf',
192
+ 'toHaveBeenCalled',
193
+ 'toHaveBeenCalledTimes',
194
+ 'toHaveBeenCalledWith',
195
+ 'toHaveBeenLastCalledWith',
196
+ 'toHaveBeenNthCalledWith',
197
+ 'toHaveReturned',
198
+ 'toHaveReturnedTimes',
199
+ 'toHaveReturnedWith',
200
+ 'toHaveLastReturnedWith',
201
+ 'toHaveNthReturnedWith',
202
+ ];
203
+
204
+ for (const matcher of matchers) {
205
+ assertionChain[matcher] = noop;
206
+ (assertionChain.not as Record<string, unknown>)[matcher] = noop;
207
+ (assertionChain.resolves as Record<string, unknown>)[matcher] = noopAsync;
208
+ (assertionChain.rejects as Record<string, unknown>)[matcher] = noopAsync;
209
+ }
210
+
211
+ const mockExpect = (_value: unknown) => assertionChain;
212
+
213
+ // Static expect methods
214
+ Object.assign(mockExpect, {
215
+ addSnapshotSerializer: noop,
216
+ any: noop,
217
+ anything: noop,
218
+ arrayContaining: noop,
219
+ assertions: noop,
220
+ closeTo: noop,
221
+ extend: noop,
222
+ getState: () => ({}),
223
+ hasAssertions: noop,
224
+ not: {
225
+ arrayContaining: noop,
226
+ objectContaining: noop,
227
+ stringContaining: noop,
228
+ stringMatching: noop,
229
+ },
230
+ objectContaining: noop,
231
+ setState: noop,
232
+ stringContaining: noop,
233
+ stringMatching: noop,
234
+ });
235
+
236
+ return mockExpect;
237
+ };
238
+
239
+ /**
240
+ * Create a mock jest object
241
+ *
242
+ * Provides stub implementations of common jest.* utilities that tests might
243
+ * call during structure capture. Since we don't execute test functions, most of
244
+ * these won't be called, but they're here for safety.
245
+ */
246
+ const createMockJestObject = (): Record<string, unknown> => {
247
+ const noop = () => {};
248
+ const noopReturnsThis = function (this: unknown) {
249
+ return this;
250
+ };
251
+
252
+ // Mock function factory
253
+ const mockFn = (implementation?: unknown) => {
254
+ const fn = typeof implementation === 'function' ? implementation : noop;
255
+ return Object.assign(fn, {
256
+ mock: { calls: [], instances: [], results: [] },
257
+ mockClear: noopReturnsThis,
258
+ mockImplementation: noopReturnsThis,
259
+ mockImplementationOnce: noopReturnsThis,
260
+ mockName: noopReturnsThis,
261
+ mockRejectedValue: noopReturnsThis,
262
+ mockRejectedValueOnce: noopReturnsThis,
263
+ mockReset: noopReturnsThis,
264
+ mockResolvedValue: noopReturnsThis,
265
+ mockResolvedValueOnce: noopReturnsThis,
266
+ mockRestore: noopReturnsThis,
267
+ mockReturnThis: noopReturnsThis,
268
+ mockReturnValue: noopReturnsThis,
269
+ mockReturnValueOnce: noopReturnsThis,
270
+ });
271
+ };
272
+
273
+ return {
274
+ advanceTimersByTime: noop,
275
+ advanceTimersByTimeAsync: () => Promise.resolve(),
276
+ advanceTimersToNextTimer: noop,
277
+ advanceTimersToNextTimerAsync: () => Promise.resolve(),
278
+ autoMockOff: noopReturnsThis,
279
+ autoMockOn: noopReturnsThis,
280
+ clearAllMocks: noop,
281
+ clearAllTimers: noop,
282
+ createMockFromModule: () => ({}),
283
+ deepUnmock: noopReturnsThis,
284
+ disableAutomock: noopReturnsThis,
285
+ doMock: noop,
286
+ dontMock: noopReturnsThis,
287
+ enableAutomock: noopReturnsThis,
288
+ fn: mockFn,
289
+ genMockFromModule: () => ({}),
290
+ getRealSystemTime: () => Date.now(),
291
+ getSeed: () => 0,
292
+ getTimerCount: () => 0,
293
+ isEnvironmentTornDown: () => false,
294
+ isMockFunction: () => false,
295
+ isolateModules: noop,
296
+ isolateModulesAsync: () => Promise.resolve(),
297
+ mock: noop,
298
+ mocked: (source: unknown) => source,
299
+ now: () => Date.now(),
300
+ replaceProperty: noopReturnsThis,
301
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return -- Mimics Jest's requireActual API
302
+ requireActual: (moduleName: string) => require(moduleName),
303
+ requireMock: () => ({}),
304
+ resetAllMocks: noop,
305
+ resetModules: noopReturnsThis,
306
+ restoreAllMocks: noop,
307
+ retryTimes: noopReturnsThis,
308
+ runAllImmediates: noop,
309
+ runAllTicks: noop,
310
+ runAllTimers: noop,
311
+ runAllTimersAsync: () => Promise.resolve(),
312
+ runOnlyPendingTimers: noop,
313
+ runOnlyPendingTimersAsync: () => Promise.resolve(),
314
+ setMock: noopReturnsThis,
315
+ setSystemTime: noop,
316
+ setTimeout: noopReturnsThis,
317
+ spyOn: () => mockFn(),
318
+ unmock: noopReturnsThis,
319
+ unstable_mockModule: noop,
320
+ useFakeTimers: noopReturnsThis,
321
+ useRealTimers: noopReturnsThis,
322
+ };
323
+ };
324
+
325
+ /**
326
+ * Initialize capture state on globalThis
327
+ */
328
+ const initCaptureState = (): CaptureState => {
329
+ const rootSuite: MutableCapturedSuite = {
330
+ children: [],
331
+ hooks: {
332
+ afterAll: [],
333
+ afterEach: [],
334
+ beforeAll: [],
335
+ beforeEach: [],
336
+ },
337
+ name: '',
338
+ tests: [],
339
+ };
340
+
341
+ const state: CaptureState = {
342
+ rootSuite,
343
+ suiteStack: [rootSuite],
344
+ };
345
+
346
+ // @ts-expect-error - intentionally using globalThis for cross-module state
347
+ globalThis[CAPTURE_STATE_KEY] = state;
348
+
349
+ return state;
350
+ };
351
+
352
+ /**
353
+ * Install Jest mocks on globalThis for module interception
354
+ */
355
+ const installJestMocks = (state: CaptureState): void => {
356
+ const mockExpect = createMockExpect();
357
+ const mockJest = createMockJestObject();
358
+
359
+ /**
360
+ * Create a describe function implementation
361
+ */
362
+ const makeDescribeFn =
363
+ (options: { only?: boolean; skip?: boolean } = {}) =>
364
+ (name: string, fn: () => void): void => {
365
+ const suite: MutableCapturedSuite = {
366
+ children: [],
367
+ hooks: {
368
+ afterAll: [],
369
+ afterEach: [],
370
+ beforeAll: [],
371
+ beforeEach: [],
372
+ },
373
+ name,
374
+ only: options.only,
375
+ skip: options.skip,
376
+ tests: [],
377
+ };
378
+
379
+ const currentSuite = getCurrentSuite(state);
380
+ currentSuite.children.push(suite);
381
+
382
+ // Push onto stack, execute callback to discover nested tests, then pop
383
+ state.suiteStack.push(suite);
384
+ try {
385
+ fn(); // Execute describe callback to discover nested content
386
+ } finally {
387
+ state.suiteStack.pop();
388
+ }
389
+ };
390
+
391
+ /**
392
+ * Create a test function implementation
393
+ */
394
+ const makeTestFn =
395
+ (options: { only?: boolean; skip?: boolean } = {}) =>
396
+ (name: string, fn?: JestTestFn, timeout?: number): void => {
397
+ const currentSuite = getCurrentSuite(state);
398
+ currentSuite.tests.push({
399
+ fn: fn ?? (() => {}),
400
+ name,
401
+ only: options.only,
402
+ skip: options.skip,
403
+ timeout,
404
+ });
405
+ };
406
+
407
+ // Create base describe function with modifiers
408
+ const describeFn = makeDescribeFn();
409
+ const describeOnly = makeDescribeFn({ only: true });
410
+ const describeSkip = makeDescribeFn({ skip: true });
411
+
412
+ const mockDescribe = Object.assign(describeFn, {
413
+ each:
414
+ (table: EachTable) =>
415
+ (nameTemplate: string, fn: (...args: unknown[]) => void): void => {
416
+ // Handle both array of arrays and template literal tagged syntax
417
+ const rows = Array.isArray(table[0]) ? table : [table];
418
+ for (const row of rows as readonly unknown[][]) {
419
+ // Simple template substitution for %s, %i, %d, %p, %j, etc.
420
+ let name = nameTemplate;
421
+ for (let i = 0; i < row.length; i++) {
422
+ name = name.replace(/%[sidpjfo#]/i, String(row[i]));
423
+ }
424
+ describeFn(name, () => fn(...row));
425
+ }
426
+ },
427
+ only: describeOnly,
428
+ skip: describeSkip,
429
+ });
430
+
431
+ // Create base test function with modifiers
432
+ const testFn = makeTestFn();
433
+ const testOnly = makeTestFn({ only: true });
434
+ const testSkip = makeTestFn({ skip: true });
435
+
436
+ const mockTest = Object.assign(testFn, {
437
+ concurrent: (name: string, fn: JestTestFn, timeout?: number): void => {
438
+ // Treat concurrent tests as regular tests for benchmarking
439
+ testFn(name, fn, timeout);
440
+ },
441
+ each:
442
+ (table: EachTable) =>
443
+ (
444
+ nameTemplate: string,
445
+ fn: (...args: unknown[]) => Promise<void> | void,
446
+ timeout?: number,
447
+ ): void => {
448
+ const rows = Array.isArray(table[0]) ? table : [table];
449
+ for (const row of rows as readonly unknown[][]) {
450
+ let name = nameTemplate;
451
+ for (let i = 0; i < row.length; i++) {
452
+ name = name.replace(/%[sidpjfo#]/i, String(row[i]));
453
+ }
454
+ testFn(name, () => fn(...row), timeout);
455
+ }
456
+ },
457
+ failing: (name: string, fn: JestTestFn, timeout?: number): void => {
458
+ // Treat failing tests as regular tests for benchmarking
459
+ testFn(name, fn, timeout);
460
+ },
461
+ only: testOnly,
462
+ skip: testSkip,
463
+ todo: (name: string): void => {
464
+ const currentSuite = getCurrentSuite(state);
465
+ currentSuite.tests.push({
466
+ fn: () => {},
467
+ name,
468
+ skip: true,
469
+ });
470
+ },
471
+ });
472
+
473
+ // Hook registration helpers
474
+ const createHook =
475
+ (hookType: 'afterAll' | 'afterEach' | 'beforeAll' | 'beforeEach') =>
476
+ (fn: JestTestFn, timeout?: number): void => {
477
+ const currentSuite = getCurrentSuite(state);
478
+ currentSuite.hooks[hookType].push({ fn, timeout });
479
+ };
480
+
481
+ // Install on globalThis for the loader to access
482
+ // @ts-expect-error - intentionally using globalThis
483
+ globalThis.__MODESTBENCH_JEST_MOCK__ = {
484
+ afterAll: createHook('afterAll'),
485
+ afterEach: createHook('afterEach'),
486
+ beforeAll: createHook('beforeAll'),
487
+ beforeEach: createHook('beforeEach'),
488
+ describe: mockDescribe,
489
+ expect: mockExpect,
490
+ fdescribe: mockDescribe.only,
491
+ fit: mockTest.only,
492
+ it: mockTest,
493
+ jest: mockJest,
494
+ test: mockTest,
495
+ xdescribe: mockDescribe.skip,
496
+ xit: mockTest.skip,
497
+ xtest: mockTest.skip,
498
+ };
499
+ };
500
+
501
+ /**
502
+ * Convert a mutable suite to the captured format
503
+ */
504
+ const convertSuite = (suite: MutableCapturedSuite): CapturedSuite => {
505
+ const hooks: SuiteHooks = {
506
+ after: suite.hooks.afterAll.map((h) => h.fn),
507
+ afterEach: suite.hooks.afterEach.map((h) => h.fn),
508
+ before: suite.hooks.beforeAll.map((h) => h.fn),
509
+ beforeEach: suite.hooks.beforeEach.map((h) => h.fn),
510
+ };
511
+
512
+ const tests: CapturedTest[] = suite.tests.map((t) => ({
513
+ fn: t.fn,
514
+ name: t.name,
515
+ only: t.only,
516
+ skip: t.skip,
517
+ }));
518
+
519
+ return {
520
+ children: suite.children.map(convertSuite),
521
+ hooks,
522
+ name: suite.name,
523
+ only: suite.only,
524
+ skip: suite.skip,
525
+ tests,
526
+ };
527
+ };
528
+
529
+ /**
530
+ * Convert capture state to CapturedTestFile
531
+ */
532
+ const toCapturedTestFile = (
533
+ state: CaptureState,
534
+ filePath: string,
535
+ ): CapturedTestFile => {
536
+ const root = state.rootSuite;
537
+
538
+ // Root-level tests (not in any describe block)
539
+ const rootTests: CapturedTest[] = root.tests.map((t) => ({
540
+ fn: t.fn,
541
+ name: t.name,
542
+ only: t.only,
543
+ skip: t.skip,
544
+ }));
545
+
546
+ // Convert child suites
547
+ const rootSuites = root.children.map(convertSuite);
548
+
549
+ return {
550
+ filePath,
551
+ framework: 'jest',
552
+ rootSuites,
553
+ rootTests,
554
+ };
555
+ };
556
+
557
+ /**
558
+ * Uninstall Jest mocks
559
+ */
560
+ const uninstallJestMocks = (): void => {
561
+ // @ts-expect-error - cleaning up globalThis
562
+ delete globalThis.__MODESTBENCH_JEST_MOCK__;
563
+ };