11import { execSync } from 'child_process'
22import fs from 'fs'
33import path from 'path'
4+ import ts from 'typescript'
45import { fileURLToPath } from 'url'
56
67const filename = fileURLToPath ( import . meta. url )
@@ -74,6 +75,25 @@ interface SuiteResult {
7475 total : number
7576}
7677
78+ const isContentAPIMode = process . env . PAYLOAD_DATABASE === 'content-api'
79+ const contentAPISuiteTimeout = 15000
80+ const vitestBinary = './node_modules/.bin/vitest'
81+
82+ function getVitestEnv ( options ?: { unsetPayloadDatabase ?: boolean } ) : NodeJS . ProcessEnv {
83+ const env = {
84+ ...process . env ,
85+ DISABLE_LOGGING : 'true' ,
86+ NODE_NO_WARNINGS : '1' ,
87+ NODE_OPTIONS : '--no-deprecation --no-experimental-strip-types' ,
88+ }
89+
90+ if ( options ?. unsetPayloadDatabase ) {
91+ delete env . PAYLOAD_DATABASE
92+ }
93+
94+ return env
95+ }
96+
7797function getTestDirectories ( ) : string [ ] {
7898 const testDir = dirname
7999
@@ -143,9 +163,11 @@ function parseTestResults(output: string): { passed: number; total: number } {
143163 try {
144164 const data = JSON . parse ( candidate )
145165 if ( typeof data . numPassedTests === 'number' && typeof data . numTotalTests === 'number' ) {
166+ const excludedTests = data . numTodoTests || 0
167+
146168 return {
147169 passed : data . numPassedTests ,
148- total : data . numTotalTests - ( data . numTodoTests || 0 ) ,
170+ total : Math . max ( 0 , data . numTotalTests - excludedTests ) ,
149171 }
150172 }
151173 } catch ( e ) {
@@ -160,6 +182,176 @@ function parseTestResults(output: string): { passed: number; total: number } {
160182 }
161183}
162184
185+ function parseCollectedTests ( output : string ) : number {
186+ const jsonStart = output . lastIndexOf ( '\n[' ) + 1 || output . indexOf ( '[' )
187+ if ( jsonStart === - 1 ) {
188+ return 0
189+ }
190+
191+ let candidate = output . substring ( jsonStart )
192+
193+ while ( candidate . length > 10 ) {
194+ try {
195+ const data = JSON . parse ( candidate )
196+
197+ if ( Array . isArray ( data ) ) {
198+ return data . length
199+ }
200+ } catch ( e ) {
201+ candidate = candidate . substring ( 0 , candidate . length - 1 )
202+ }
203+ }
204+
205+ return 0
206+ }
207+
208+ function getCollectedTestCount ( suiteName : string ) : number {
209+ const testPath = path . join ( dirname , suiteName , 'int.spec.ts' )
210+
211+ for ( const unsetPayloadDatabase of [ false , true ] ) {
212+ try {
213+ const command = `${ vitestBinary } list --project int ${ testPath } --json`
214+
215+ const output = execSync ( command , {
216+ cwd : path . join ( dirname , '..' ) ,
217+ encoding : 'utf8' ,
218+ env : getVitestEnv ( { unsetPayloadDatabase } ) ,
219+ stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
220+ ...( isContentAPIMode ? { timeout : contentAPISuiteTimeout } : { } ) ,
221+ } )
222+
223+ const count = parseCollectedTests ( output )
224+ if ( count > 0 ) {
225+ return count
226+ }
227+ } catch ( error : unknown ) {
228+ let errorOutput = ''
229+ if ( error && typeof error === 'object' ) {
230+ if ( 'stdout' in error ) {
231+ const stdout = ( error as { stdout ?: unknown } ) . stdout
232+ if ( typeof stdout === 'string' ) {
233+ errorOutput += stdout
234+ } else if ( stdout && Buffer . isBuffer ( stdout ) ) {
235+ errorOutput += stdout . toString ( 'utf8' )
236+ }
237+ }
238+ if ( 'stderr' in error ) {
239+ const stderr = ( error as { stderr ?: unknown } ) . stderr
240+ if ( typeof stderr === 'string' ) {
241+ errorOutput += '\n' + stderr
242+ } else if ( stderr && Buffer . isBuffer ( stderr ) ) {
243+ errorOutput += '\n' + stderr . toString ( 'utf8' )
244+ }
245+ }
246+ }
247+
248+ const count = parseCollectedTests ( errorOutput )
249+ if ( count > 0 ) {
250+ return count
251+ }
252+ }
253+ }
254+
255+ return 0
256+ }
257+
258+ function isBaseTestIdentifier ( node : ts . Node ) : node is ts . Identifier {
259+ return ts . isIdentifier ( node ) && ( node . text === 'it' || node . text === 'test' )
260+ }
261+
262+ function isRunnableTestCall ( node : ts . CallExpression ) : boolean {
263+ const expression = node . expression
264+
265+ if ( isBaseTestIdentifier ( expression ) ) {
266+ return true
267+ }
268+
269+ if ( ts . isPropertyAccessExpression ( expression ) ) {
270+ if ( ! isBaseTestIdentifier ( expression . expression ) ) {
271+ return false
272+ }
273+
274+ return (
275+ expression . name . text !== 'skip' &&
276+ expression . name . text !== 'todo' &&
277+ expression . name . text !== 'each'
278+ )
279+ }
280+
281+ if ( ts . isCallExpression ( expression ) && ts . isPropertyAccessExpression ( expression . expression ) ) {
282+ const innerExpression = expression . expression
283+
284+ return isBaseTestIdentifier ( innerExpression . expression ) && innerExpression . name . text === 'each'
285+ }
286+
287+ return false
288+ }
289+
290+ function getStaticTestCount ( suiteName : string ) : number {
291+ const testPath = path . join ( dirname , suiteName , 'int.spec.ts' )
292+ const sourceText = fs . readFileSync ( testPath , 'utf8' )
293+ const sourceFile = ts . createSourceFile (
294+ testPath ,
295+ sourceText ,
296+ ts . ScriptTarget . Latest ,
297+ true ,
298+ ts . ScriptKind . TS ,
299+ )
300+ let count = 0
301+
302+ function visit ( node : ts . Node ) {
303+ if ( ts . isCallExpression ( node ) && isRunnableTestCall ( node ) ) {
304+ count ++
305+ }
306+
307+ ts . forEachChild ( node , visit )
308+ }
309+
310+ visit ( sourceFile )
311+
312+ return count
313+ }
314+
315+ function getExplicitSkippedTestCount ( suiteName : string ) : number {
316+ const testPath = path . join ( dirname , suiteName , 'int.spec.ts' )
317+ const sourceText = fs . readFileSync ( testPath , 'utf8' )
318+ const sourceFile = ts . createSourceFile (
319+ testPath ,
320+ sourceText ,
321+ ts . ScriptTarget . Latest ,
322+ true ,
323+ ts . ScriptKind . TS ,
324+ )
325+ let count = 0
326+
327+ function visit ( node : ts . Node ) {
328+ if (
329+ ts . isCallExpression ( node ) &&
330+ ts . isIdentifier ( node . expression ) &&
331+ node . expression . text === 'mongoIt' &&
332+ process . env . PAYLOAD_DATABASE !== 'mongodb'
333+ ) {
334+ count ++
335+ }
336+
337+ if ( ts . isCallExpression ( node ) && ts . isPropertyAccessExpression ( node . expression ) ) {
338+ const { expression, name } = node . expression
339+ if (
340+ ts . isIdentifier ( expression ) &&
341+ ( expression . text === 'it' || expression . text === 'test' ) &&
342+ name . text === 'skip'
343+ ) {
344+ count ++
345+ }
346+ }
347+
348+ ts . forEachChild ( node , visit )
349+ }
350+
351+ visit ( sourceFile )
352+ return count
353+ }
354+
163355function runTestSuite ( suiteName : string ) : SuiteResult {
164356 const startTime = Date . now ( )
165357 const result : SuiteResult = {
@@ -172,18 +364,27 @@ function runTestSuite(suiteName: string): SuiteResult {
172364
173365 try {
174366 const testPath = path . join ( dirname , suiteName , 'int.spec.ts' )
175- const command = `cross-env NODE_OPTIONS="--no-deprecation --no-experimental-strip-types" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true vitest run --project int ${ testPath } --reporter=json`
367+ const command = `${ vitestBinary } run --project int ${ testPath } --reporter=json`
176368
177369 const output = execSync ( command , {
178370 cwd : path . join ( dirname , '..' ) ,
179371 encoding : 'utf8' ,
372+ env : getVitestEnv ( ) ,
180373 stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
374+ ...( isContentAPIMode ? { timeout : contentAPISuiteTimeout } : { } ) ,
181375 } )
182376
183377 // Parse Jest output to extract test counts
184378 const parsed = parseTestResults ( output )
185379 result . passed = parsed . passed
186380 result . total = parsed . total
381+
382+ if ( result . total === 0 ) {
383+ result . total = getCollectedTestCount ( suiteName )
384+ }
385+ if ( result . total === 0 ) {
386+ result . total = getStaticTestCount ( suiteName )
387+ }
187388 } catch ( error : unknown ) {
188389 // Try to parse failure output from both stdout and stderr
189390 let errorOutput = ''
@@ -210,11 +411,16 @@ function runTestSuite(suiteName: string): SuiteResult {
210411 result . passed = parsed . passed
211412 result . total = parsed . total
212413
213- // Only mark as failed if tests actually failed (not all passed)
214- // Some tests may exit with error code even if all tests pass
215- result . failed = result . passed < result . total
414+ if ( result . total === 0 ) {
415+ result . total = getCollectedTestCount ( suiteName )
416+ }
417+ if ( result . total === 0 ) {
418+ result . total = getStaticTestCount ( suiteName )
419+ }
216420 }
217421
422+ result . total = Math . max ( 0 , result . total - getExplicitSkippedTestCount ( suiteName ) )
423+ result . failed = result . passed < result . total
218424 result . duration = Date . now ( ) - startTime
219425 return result
220426}
0 commit comments