11import type { File , Suite , Task , Test } from '@vitest/runner'
2- import type { Property } from 'estree'
32import type { SerializedConfig } from '../runtime/config'
43import type { TestError } from '../types/general'
54import type { TestProject } from './project'
@@ -46,6 +45,8 @@ interface LocalCallDefinition {
4645 mode : 'run' | 'skip' | 'only' | 'todo' | 'queued'
4746 task : ParsedSuite | ParsedFile | ParsedTest
4847 dynamic : boolean
48+ concurrent : boolean
49+ sequential : boolean
4950 tags : string [ ]
5051}
5152
@@ -103,8 +104,8 @@ function astParseFile(filepath: string, code: string) {
103104 ) {
104105 return getName ( callee . property )
105106 }
106- // call as `__vite_ssr__.test.skip()`
107- return getName ( callee . object ?. property )
107+ // call as `__vite_ssr__.test.skip()` or `describe.concurrent.each()`
108+ return getName ( callee . object )
108109 }
109110 // unwrap (0, ...)
110111 if ( callee . type === 'SequenceExpression' && callee . expressions . length === 2 ) {
@@ -116,6 +117,29 @@ function astParseFile(filepath: string, code: string) {
116117 return null
117118 }
118119
120+ const getProperties = ( callee : any ) : string [ ] => {
121+ if ( ! callee ) {
122+ return [ ]
123+ }
124+ if ( callee . type === 'Identifier' ) {
125+ return [ ]
126+ }
127+ if ( callee . type === 'CallExpression' ) {
128+ return getProperties ( callee . callee )
129+ }
130+ if ( callee . type === 'TaggedTemplateExpression' ) {
131+ return getProperties ( callee . tag )
132+ }
133+ if ( callee . type === 'MemberExpression' ) {
134+ const props = getProperties ( callee . object )
135+ if ( callee . property ?. name ) {
136+ props . push ( callee . property . name )
137+ }
138+ return props
139+ }
140+ return [ ]
141+ }
142+
119143 walkAst ( ast as any , {
120144 CallExpression ( node ) {
121145 const { callee } = node as any
@@ -127,12 +151,24 @@ function astParseFile(filepath: string, code: string) {
127151 verbose ?.( `Skipping ${ name } (unknown call)` )
128152 return
129153 }
154+ const properties = getProperties ( callee )
130155 const property = callee ?. property ?. name
131- let mode = ! property || property === name ? 'run' : property
132- // they will be picked up in the next iteration
133- if ( [ 'each' , 'for' , 'skipIf' , 'runIf' , 'extend' , 'scoped' , 'override' ] . includes ( mode ) ) {
156+ // intermediate calls like .each(), .for() will be picked up in the next iteration
157+ if ( property && [ 'each' , 'for' , 'skipIf' , 'runIf' , 'extend' , 'scoped' , 'override' ] . includes ( property ) ) {
134158 return
135159 }
160+ // derive mode from the full chain (handles any order like .skip.concurrent or .concurrent.skip)
161+ let mode : 'run' | 'skip' | 'only' | 'todo' = 'run'
162+ for ( const prop of properties ) {
163+ if ( prop === 'skip' || prop === 'only' || prop === 'todo' ) {
164+ mode = prop
165+ }
166+ else if ( prop === 'skipIf' || prop === 'runIf' ) {
167+ mode = 'skip'
168+ }
169+ }
170+ let isConcurrent = properties . includes ( 'concurrent' )
171+ let isSequential = properties . includes ( 'sequential' )
136172
137173 let start : number
138174 const end = node . end
@@ -179,39 +215,46 @@ function astParseFile(filepath: string, code: string) {
179215 // Vitest module mocker injects these
180216 . replace ( / _ _ v i _ i m p o r t _ \d + _ _ \. / g, '' )
181217
182- // cannot statically analyze, so we always skip it
183- if ( mode === 'skipIf' || mode === 'runIf' ) {
184- mode = 'skip'
185- }
186-
187218 const parentCalleeName = typeof callee ?. callee === 'object' && callee ?. callee . type === 'MemberExpression' && callee ?. callee . property ?. name
188219 let isDynamicEach = parentCalleeName === 'each' || parentCalleeName === 'for'
189220 if ( ! isDynamicEach && callee . type === 'TaggedTemplateExpression' ) {
190221 const property = callee . tag ?. property ?. name
191222 isDynamicEach = property === 'each' || property === 'for'
192223 }
193224
194- // Extract tags from the second argument if it's an options object
225+ // Extract options from the second argument if it's an options object
195226 const tags : string [ ] = [ ]
196227 const secondArg = node . arguments ?. [ 1 ]
197228 if ( secondArg ?. type === 'ObjectExpression' ) {
198- const tagsProperty = secondArg . properties ?. find (
199- ( p : any ) => p . type === 'Property' && p . key ?. type === 'Identifier' && p . key . name === 'tags' ,
200- ) as Property | undefined
201- if ( tagsProperty ) {
202- const tagsValue = tagsProperty . value
203- if ( tagsValue ?. type === 'Literal' && typeof tagsValue . value === 'string' ) {
204- // tags: 'single-tag'
205- tags . push ( tagsValue . value )
229+ for ( const prop of ( secondArg . properties || [ ] ) as any [ ] ) {
230+ if ( prop . type !== 'Property' || prop . key ?. type !== 'Identifier' ) {
231+ continue
206232 }
207- else if ( tagsValue ?. type === 'ArrayExpression' ) {
208- // tags: ['tag1', 'tag2']
209- for ( const element of tagsValue . elements || [ ] ) {
210- if ( element ?. type === 'Literal' && typeof element . value === 'string' ) {
211- tags . push ( element . value )
233+ const keyName = prop . key . name
234+ if ( keyName === 'tags' ) {
235+ const tagsValue = prop . value
236+ if ( tagsValue ?. type === 'Literal' && typeof tagsValue . value === 'string' ) {
237+ tags . push ( tagsValue . value )
238+ }
239+ else if ( tagsValue ?. type === 'ArrayExpression' ) {
240+ for ( const element of tagsValue . elements || [ ] ) {
241+ if ( element ?. type === 'Literal' && typeof element . value === 'string' ) {
242+ tags . push ( element . value )
243+ }
212244 }
213245 }
214246 }
247+ else if ( prop . value ?. type === 'Literal' && prop . value . value === true ) {
248+ if ( keyName === 'skip' || keyName === 'only' || keyName === 'todo' ) {
249+ mode = keyName
250+ }
251+ else if ( keyName === 'concurrent' ) {
252+ isConcurrent = true
253+ }
254+ else if ( keyName === 'sequential' ) {
255+ isSequential = true
256+ }
257+ }
215258 }
216259 }
217260
@@ -224,6 +267,8 @@ function astParseFile(filepath: string, code: string) {
224267 mode,
225268 task : null as any ,
226269 dynamic : isDynamicEach ,
270+ concurrent : isConcurrent ,
271+ sequential : isSequential ,
227272 tags,
228273 } satisfies LocalCallDefinition )
229274 } ,
@@ -366,6 +411,10 @@ function createFileTask(
366411 // Inherit tags from parent suite and merge with own tags
367412 const parentTags = latestSuite . tags || [ ]
368413 const taskTags = unique ( [ ...parentTags , ...definition . tags ] )
414+ // resolve concurrent/sequential: sequential cancels inherited concurrent
415+ const concurrent = definition . sequential
416+ ? undefined
417+ : ( definition . concurrent || latestSuite . concurrent || undefined )
369418
370419 if ( definition . type === 'suite' ) {
371420 const task : ParsedSuite = {
@@ -376,6 +425,7 @@ function createFileTask(
376425 tasks : [ ] ,
377426 mode,
378427 each : definition . dynamic ,
428+ concurrent,
379429 name : definition . name ,
380430 fullName : createTaskName ( [ latestSuite . fullName , definition . name ] ) ,
381431 fullTestName : createTaskName ( [ latestSuite . fullTestName , definition . name ] ) ,
@@ -398,6 +448,7 @@ function createFileTask(
398448 suite : latestSuite ,
399449 file,
400450 each : definition . dynamic ,
451+ concurrent,
401452 mode,
402453 context : { } as any , // not used on the server
403454 name : definition . name ,
0 commit comments