2424 * NOTE: This runner requires jest-runner to be installed in your project.
2525 * It is a Jest-specific feature and does not work with Vitest.
2626 * For Vitest projects, capturePerf() does all loops internally in a single call.
27+ *
28+ * Compatibility: Works with Jest 29.x and Jest 30.x
2729 */
2830
2931'use strict' ;
3032
3133const { createRequire } = require ( 'module' ) ;
3234const path = require ( 'path' ) ;
3335
34- // Try to load jest-runner - it's a peer dependency that must be installed by the user
36+ // Try to load jest-runner from the PROJECT's node_modules, not from codeflash package
37+ // This ensures we use the same version of jest-runner that the project uses
38+ let TestRunner ;
3539let runTest ;
3640let jestRunnerAvailable = false ;
41+ let jestVersion = 0 ;
3742
3843try {
39- const jestRunnerPath = require . resolve ( 'jest-runner' ) ;
44+ // Resolve jest-runner from the current working directory (project root)
45+ // This is important because the codeflash package may bundle a different version
46+ const projectRoot = process . cwd ( ) ;
47+ const projectRequire = createRequire ( path . join ( projectRoot , 'node_modules' , 'package.json' ) ) ;
48+
49+ let jestRunnerPath ;
50+ try {
51+ // First try to resolve from project's node_modules
52+ jestRunnerPath = projectRequire . resolve ( 'jest-runner' ) ;
53+ } catch ( e ) {
54+ // Fall back to default resolution (codeflash's bundled version)
55+ jestRunnerPath = require . resolve ( 'jest-runner' ) ;
56+ }
57+
4058 const internalRequire = createRequire ( jestRunnerPath ) ;
41- runTest = internalRequire ( './runTest' ) . default ;
42- jestRunnerAvailable = true ;
59+
60+ // Try to get the TestRunner class (Jest 30+)
61+ const jestRunner = internalRequire ( jestRunnerPath ) ;
62+ TestRunner = jestRunner . default || jestRunner . TestRunner ;
63+
64+ if ( TestRunner && TestRunner . prototype && typeof TestRunner . prototype . runTests === 'function' ) {
65+ // Jest 30+ - use TestRunner class
66+ jestVersion = 30 ;
67+ jestRunnerAvailable = true ;
68+ } else {
69+ // Try Jest 29 style import
70+ try {
71+ runTest = internalRequire ( './runTest' ) . default ;
72+ if ( typeof runTest === 'function' ) {
73+ jestVersion = 29 ;
74+ jestRunnerAvailable = true ;
75+ }
76+ } catch ( e29 ) {
77+ // Neither Jest 29 nor 30 style import worked
78+ jestRunnerAvailable = false ;
79+ }
80+ }
4381} catch ( e ) {
4482 // jest-runner not installed - this is expected for Vitest projects
4583 // The runner will throw a helpful error if someone tries to use it without jest-runner
@@ -106,6 +144,9 @@ function deepCopy(obj, seen = new WeakMap()) {
106144
107145/**
108146 * Codeflash Loop Runner with Batched Looping
147+ *
148+ * For Jest 30+, extends the TestRunner class directly.
149+ * For Jest 29, uses the runTest function import.
109150 */
110151class CodeflashLoopRunner {
111152 constructor ( globalConfig , context ) {
@@ -120,6 +161,11 @@ class CodeflashLoopRunner {
120161 this . _globalConfig = globalConfig ;
121162 this . _context = context || { } ;
122163 this . _eventEmitter = new SimpleEventEmitter ( ) ;
164+
165+ // For Jest 30+, create an instance of the base TestRunner for delegation
166+ if ( jestVersion >= 30 && TestRunner ) {
167+ this . _baseRunner = new TestRunner ( globalConfig , context ) ;
168+ }
123169 }
124170
125171 get supportsEventEmitters ( ) {
@@ -143,29 +189,20 @@ class CodeflashLoopRunner {
143189 let hasFailure = false ;
144190 let allConsoleOutput = '' ;
145191
146- // Import shared state functions from capture module
147- // We need to do this dynamically since the module may be reloaded
148- let checkSharedTimeLimit ;
149- let incrementBatch ;
150- try {
151- const capture = require ( 'codeflash' ) ;
152- checkSharedTimeLimit = capture . checkSharedTimeLimit ;
153- incrementBatch = capture . incrementBatch ;
154- } catch ( e ) {
155- // Fallback if codeflash module not available
156- checkSharedTimeLimit = ( ) => {
157- const elapsed = Date . now ( ) - startTime ;
158- return elapsed >= TARGET_DURATION_MS && batchCount >= MIN_BATCHES ;
159- } ;
160- incrementBatch = ( ) => { } ;
161- }
192+ // Time limit check - must use local time tracking because Jest runs tests
193+ // in worker processes, so shared state from capture.js isn't accessible here
194+ const checkTimeLimit = ( ) => {
195+ const elapsed = Date . now ( ) - startTime ;
196+ return elapsed >= TARGET_DURATION_MS && batchCount >= MIN_BATCHES ;
197+ } ;
162198
163199 // Batched looping: run all test files multiple times
164200 while ( batchCount < MAX_BATCHES ) {
165201 batchCount ++ ;
166202
167203 // Check time limit BEFORE each batch
168- if ( batchCount > MIN_BATCHES && checkSharedTimeLimit ( ) ) {
204+ if ( batchCount > MIN_BATCHES && checkTimeLimit ( ) ) {
205+ console . log ( `[codeflash] Time limit reached after ${ batchCount - 1 } batches (${ Date . now ( ) - startTime } ms elapsed)` ) ;
169206 break ;
170207 }
171208
@@ -174,13 +211,11 @@ class CodeflashLoopRunner {
174211 break ;
175212 }
176213
177- // Increment batch counter in shared state and set env var
178- // The env var persists across Jest module resets, ensuring continuous loop indices
179- incrementBatch ( ) ;
214+ // Set env var for batch number - persists across Jest module resets
180215 process . env . CODEFLASH_PERF_CURRENT_BATCH = String ( batchCount ) ;
181216
182217 // Run all test files in this batch
183- const batchResult = await this . _runAllTestsOnce ( tests , watcher ) ;
218+ const batchResult = await this . _runAllTestsOnce ( tests , watcher , options ) ;
184219 allConsoleOutput += batchResult . consoleOutput ;
185220
186221 if ( batchResult . hasFailure ) {
@@ -189,7 +224,8 @@ class CodeflashLoopRunner {
189224 }
190225
191226 // Check time limit AFTER each batch
192- if ( checkSharedTimeLimit ( ) ) {
227+ if ( checkTimeLimit ( ) ) {
228+ console . log ( `[codeflash] Time limit reached after ${ batchCount } batches (${ Date . now ( ) - startTime } ms elapsed)` ) ;
193229 break ;
194230 }
195231 }
@@ -207,8 +243,74 @@ class CodeflashLoopRunner {
207243
208244 /**
209245 * Run all test files once (one batch).
246+ * Uses different approaches for Jest 29 vs Jest 30.
247+ */
248+ async _runAllTestsOnce ( tests , watcher , options ) {
249+ if ( jestVersion >= 30 ) {
250+ return this . _runAllTestsOnceJest30 ( tests , watcher , options ) ;
251+ } else {
252+ return this . _runAllTestsOnceJest29 ( tests , watcher ) ;
253+ }
254+ }
255+
256+ /**
257+ * Jest 30+ implementation - delegates to base TestRunner and collects results.
258+ */
259+ async _runAllTestsOnceJest30 ( tests , watcher , options ) {
260+ let hasFailure = false ;
261+ let allConsoleOutput = '' ;
262+
263+ // For Jest 30, we need to collect results through event listeners
264+ const resultsCollector = [ ] ;
265+
266+ // Subscribe to events from the base runner
267+ const unsubscribeSuccess = this . _baseRunner . on ( 'test-file-success' , ( testData ) => {
268+ const [ test , result ] = testData ;
269+ resultsCollector . push ( { test, result, success : true } ) ;
270+
271+ if ( result && result . console && Array . isArray ( result . console ) ) {
272+ allConsoleOutput += result . console . map ( e => e . message || '' ) . join ( '\n' ) + '\n' ;
273+ }
274+
275+ if ( result && result . numFailingTests > 0 ) {
276+ hasFailure = true ;
277+ }
278+
279+ // Forward to our event emitter
280+ this . _eventEmitter . emit ( 'test-file-success' , testData ) ;
281+ } ) ;
282+
283+ const unsubscribeFailure = this . _baseRunner . on ( 'test-file-failure' , ( testData ) => {
284+ const [ test , error ] = testData ;
285+ resultsCollector . push ( { test, error, success : false } ) ;
286+ hasFailure = true ;
287+
288+ // Forward to our event emitter
289+ this . _eventEmitter . emit ( 'test-file-failure' , testData ) ;
290+ } ) ;
291+
292+ const unsubscribeStart = this . _baseRunner . on ( 'test-file-start' , ( testData ) => {
293+ // Forward to our event emitter
294+ this . _eventEmitter . emit ( 'test-file-start' , testData ) ;
295+ } ) ;
296+
297+ try {
298+ // Run tests using the base runner (always serial for benchmarking)
299+ await this . _baseRunner . runTests ( tests , watcher , { ...options , serial : true } ) ;
300+ } finally {
301+ // Cleanup subscriptions
302+ if ( typeof unsubscribeSuccess === 'function' ) unsubscribeSuccess ( ) ;
303+ if ( typeof unsubscribeFailure === 'function' ) unsubscribeFailure ( ) ;
304+ if ( typeof unsubscribeStart === 'function' ) unsubscribeStart ( ) ;
305+ }
306+
307+ return { consoleOutput : allConsoleOutput , hasFailure } ;
308+ }
309+
310+ /**
311+ * Jest 29 implementation - uses direct runTest import.
210312 */
211- async _runAllTestsOnce ( tests , watcher ) {
313+ async _runAllTestsOnceJest29 ( tests , watcher ) {
212314 let hasFailure = false ;
213315 let allConsoleOutput = '' ;
214316
0 commit comments