@@ -8,9 +8,15 @@ import {
88} from 'vscode' ;
99import { testFileCache } from './testDetection/testFileCache' ;
1010import { CodeLensOption } from './util' ;
11- import { escapeRegExp , findFullTestName , resolveTestNameStringInterpolation , TestNode } from './utils/TestNameUtils' ;
11+ import {
12+ findFullTestName ,
13+ toTestNamePattern ,
14+ TestNode ,
15+ } from './utils/TestNameUtils' ;
1216import { logError } from './utils/Logger' ;
1317
18+ type LensNode = TestNode & { eachTemplate ?: string ; children ?: LensNode [ ] } ;
19+
1420const CODE_LENS_CONFIG : Record <
1521 CodeLensOption ,
1622 { title : string ; command : string }
@@ -39,41 +45,170 @@ function getCodeLensForOption(
3945 } ) ;
4046}
4147
42- function buildFullTestName ( node : TestNode , parseResults : TestNode [ ] ) : string {
43- const parents : string [ ] = [ ] ;
44-
45- function findParents (
46- searchNode : TestNode ,
47- target : TestNode ,
48- currentPath : string [ ] = [ ] ,
49- ) : string [ ] | null {
50- if ( searchNode === target ) {
51- return currentPath ;
52- }
53- if ( searchNode . children ) {
54- for ( const child of searchNode . children ) {
55- const path = searchNode . name
56- ? [ ...currentPath , searchNode . name ]
57- : currentPath ;
58- const result = findParents ( child , target , path ) ;
59- if ( result ) return result ;
60- }
61- }
48+ const getEachTemplate = ( node ?: TestNode ) : string | undefined => {
49+ const lensNode = node as LensNode | undefined ;
50+ return lensNode ?. eachTemplate ;
51+ } ;
52+
53+ const hasEachTemplate = ( node ?: TestNode ) : boolean =>
54+ Boolean ( getEachTemplate ( node ) ) ;
55+
56+ const sortByStartColumn = ( nodes : TestNode [ ] ) : TestNode [ ] =>
57+ [ ...nodes ] . sort ( ( a , b ) => ( a . start ?. column || 0 ) - ( b . start ?. column || 0 ) ) ;
58+
59+ const getNodeRange = ( node : TestNode ) : Range =>
60+ new Range (
61+ node . start . line - 1 ,
62+ node . start . column ,
63+ node . end . line - 1 ,
64+ node . end . column ,
65+ ) ;
66+
67+ const isSameLineEachNode =
68+ ( baseNode : TestNode ) =>
69+ ( candidate : TestNode ) : boolean =>
70+ candidate . type === baseNode . type &&
71+ hasEachTemplate ( candidate ) &&
72+ candidate . start ?. line === baseNode . start ?. line ;
73+
74+ const isNestedItInsideDescribeEach = (
75+ node : TestNode ,
76+ parent ?: TestNode ,
77+ ) : boolean =>
78+ node . type === 'it' && parent ?. type === 'describe' && hasEachTemplate ( parent ) ;
79+
80+ const findParentPath = (
81+ searchNode : TestNode ,
82+ target : TestNode ,
83+ currentPath : string [ ] = [ ] ,
84+ ) : string [ ] | null => {
85+ if ( searchNode === target ) {
86+ return currentPath ;
87+ }
88+
89+ if ( ! searchNode . children ) {
6290 return null ;
6391 }
6492
93+ for ( const child of searchNode . children ) {
94+ const nextPath = searchNode . name
95+ ? [ ...currentPath , searchNode . name ]
96+ : currentPath ;
97+ const result = findParentPath ( child , target , nextPath ) ;
98+ if ( result ) {
99+ return result ;
100+ }
101+ }
102+
103+ return null ;
104+ } ;
105+
106+ function buildFullTestName ( node : TestNode , parseResults : TestNode [ ] ) : string {
65107 for ( const root of parseResults ) {
66- const parentPath = findParents ( root , node ) ;
108+ const parentPath = findParentPath ( root , node ) ;
67109 if ( parentPath ) {
68- parents . push ( ...parentPath ) ;
69- break ;
110+ return [ ...parentPath , node . name || '' ] . filter ( Boolean ) . join ( ' ' ) ;
70111 }
71112 }
72113
73- const fullPath = [ ...parents , node . name || '' ] . filter ( Boolean ) ;
74- return fullPath . join ( ' ' ) ;
114+ return node . name || '' ;
75115}
76116
117+ const getSiblingEachNodes = (
118+ node : TestNode ,
119+ parent : TestNode | undefined ,
120+ parseResults : TestNode [ ] ,
121+ ) : TestNode [ ] => {
122+ const siblings = parent ?. children || parseResults ;
123+ return siblings . filter ( isSameLineEachNode ( node ) ) ;
124+ } ;
125+
126+ const getNestedItEachNodes = (
127+ node : TestNode ,
128+ parent : TestNode ,
129+ parseResults : TestNode [ ] ,
130+ ) : TestNode [ ] => {
131+ const parentTemplate = getEachTemplate ( parent ) ;
132+ const childTemplate = getEachTemplate ( node ) ;
133+
134+ if ( ! parentTemplate || ! childTemplate ) {
135+ return [ ] ;
136+ }
137+
138+ const describeRows = parseResults . filter (
139+ ( candidate ) =>
140+ candidate . type === 'describe' &&
141+ candidate . start ?. line === parent . start ?. line &&
142+ getEachTemplate ( candidate ) === parentTemplate ,
143+ ) ;
144+
145+ return describeRows
146+ . map ( ( describeNode ) =>
147+ describeNode . children ?. find (
148+ ( child ) =>
149+ child . type === 'it' &&
150+ child . start ?. line === node . start ?. line &&
151+ getEachTemplate ( child ) === childTemplate ,
152+ ) ,
153+ )
154+ . filter ( ( candidate ) : candidate is TestNode => Boolean ( candidate ) ) ;
155+ } ;
156+
157+ const getGroupedEachNodes = (
158+ node : TestNode ,
159+ parent : TestNode | undefined ,
160+ parseResults : TestNode [ ] ,
161+ ) : TestNode [ ] => {
162+ if ( parent && isNestedItInsideDescribeEach ( node , parent ) ) {
163+ return getNestedItEachNodes ( node , parent , parseResults ) ;
164+ }
165+
166+ return getSiblingEachNodes ( node , parent , parseResults ) ;
167+ } ;
168+
169+ const buildGroupKey = (
170+ node : TestNode ,
171+ parent : TestNode | undefined ,
172+ nestedInDescribeEach : boolean ,
173+ ) : string => {
174+ if ( nestedInDescribeEach ) {
175+ return `it-in-describe-each-${ parent ?. start ?. line } -${ node . start ?. line } -${ getEachTemplate ( parent ) } ` ;
176+ }
177+
178+ return `${ node . start ?. line } -${ parent ?. name || 'root' } ` ;
179+ } ;
180+
181+ const buildAllPatternName = (
182+ node : TestNode ,
183+ parent : TestNode | undefined ,
184+ parseResults : TestNode [ ] ,
185+ nestedInDescribeEach : boolean ,
186+ ) : string | undefined => {
187+ const template = getEachTemplate ( node ) ;
188+ if ( template ) {
189+ const parentTemplateOrName = nestedInDescribeEach
190+ ? getEachTemplate ( parent ) || parent ?. name
191+ : parent ?. name ;
192+
193+ const fullTemplateName = [ parentTemplateOrName , template ]
194+ . filter ( Boolean )
195+ . join ( ' ' ) ;
196+ return toTestNamePattern ( fullTemplateName ) ;
197+ }
198+
199+ const line = node . start ?. line ;
200+ if ( ! line ) {
201+ return undefined ;
202+ }
203+
204+ return toTestNamePattern ( findFullTestName ( line , parseResults ) ) ;
205+ } ;
206+
207+ const getIndexedTitle = ( option : CodeLensOption , index ?: number ) : string => {
208+ const baseTitle = CODE_LENS_CONFIG [ option ] . title ;
209+ return index !== undefined ? `[${ index } ] ${ baseTitle } ` : baseTitle ;
210+ } ;
211+
77212function getTestsBlocks (
78213 parsedNode : TestNode ,
79214 parseResults : TestNode [ ] ,
@@ -89,49 +224,55 @@ function getTestsBlocks(
89224 const groupsSet = processedGroups || new Set < string > ( ) ;
90225
91226 parsedNode . children ?. forEach ( ( subNode ) => {
92- codeLens . push ( ...getTestsBlocks ( subNode , parseResults , codeLensOptions , parsedNode , groupsSet ) ) ;
227+ codeLens . push (
228+ ...getTestsBlocks (
229+ subNode ,
230+ parseResults ,
231+ codeLensOptions ,
232+ parsedNode ,
233+ groupsSet ,
234+ ) ,
235+ ) ;
93236 } ) ;
94237
95- const range = new Range (
96- parsedNode . start . line - 1 ,
97- parsedNode . start . column ,
98- parsedNode . end . line - 1 ,
99- parsedNode . end . column ,
100- ) ;
238+ if ( ! parsedNode . start || ! parsedNode . end ) {
239+ return codeLens ;
240+ }
101241
102- const fullTestName = escapeRegExp ( buildFullTestName ( parsedNode , parseResults ) ) ;
242+ const range = getNodeRange ( parsedNode ) ;
243+
244+ const fullTestName =
245+ toTestNamePattern ( buildFullTestName ( parsedNode , parseResults ) ) || '' ;
103246
104247 let testIndex : number | undefined ;
105- if ( parsedNode . type === 'it' && parsedNode . start ) {
106- const siblings = parent ?. children || parseResults ;
107- const sameLineTests = siblings . filter (
108- ( node ) =>
109- node . type === 'it' &&
110- node . start ?. line === parsedNode . start ?. line ,
248+ const isExpandedEachNode = hasEachTemplate ( parsedNode ) ;
249+ const supportsEachGrouping =
250+ parsedNode . start &&
251+ isExpandedEachNode &&
252+ ( parsedNode . type === 'it' || parsedNode . type === 'describe' ) ;
253+
254+ if ( supportsEachGrouping ) {
255+ const nestedInDescribeEach = isNestedItInsideDescribeEach (
256+ parsedNode ,
257+ parent ,
111258 ) ;
259+ const sameLineTests = getGroupedEachNodes ( parsedNode , parent , parseResults ) ;
112260
113261 if ( sameLineTests . length > 1 ) {
114- const groupKey = ` ${ parsedNode . start . line } - ${ parent ?. name || 'root' } ` ;
262+ const groupKey = buildGroupKey ( parsedNode , parent , nestedInDescribeEach ) ;
115263
116- const sortedTests = [ ...sameLineTests ] . sort (
117- ( a , b ) => ( a . start ?. column || 0 ) - ( b . start ?. column || 0 ) ,
118- ) ;
264+ const sortedTests = sortByStartColumn ( sameLineTests ) ;
119265 testIndex = sortedTests . indexOf ( parsedNode ) + 1 ;
120266
121267 if ( ! groupsSet . has ( groupKey ) ) {
122268 groupsSet . add ( groupKey ) ;
123269
124- let patternName : string | undefined ;
125- if ( ( parsedNode as any ) . eachTemplate ) {
126- const template = ( parsedNode as any ) . eachTemplate ;
127- const parentNames = parent ?. name ? [ parent . name ] : [ ] ;
128- const fullTemplateName = [ ...parentNames , template ] . filter ( Boolean ) . join ( ' ' ) ;
129- patternName = escapeRegExp ( resolveTestNameStringInterpolation ( fullTemplateName ) ) ;
130- } else {
131- patternName = escapeRegExp (
132- findFullTestName ( parsedNode . start . line , parseResults ) || '' ,
133- ) ;
134- }
270+ const patternName = buildAllPatternName (
271+ parsedNode ,
272+ parent ,
273+ parseResults ,
274+ nestedInDescribeEach ,
275+ ) ;
135276
136277 if ( patternName ) {
137278 codeLens . push (
@@ -145,8 +286,7 @@ function getTestsBlocks(
145286
146287 codeLens . push (
147288 ...codeLensOptions . map ( ( option ) => {
148- const config = CODE_LENS_CONFIG [ option ] ;
149- const title = testIndex !== undefined ? `[${ testIndex } ] ${ config . title } ` : config . title ;
289+ const title = getIndexedTitle ( option , testIndex ) ;
150290 return getCodeLensForOption ( range , option , fullTestName , title ) ;
151291 } ) ,
152292 ) ;
@@ -157,22 +297,33 @@ function getTestsBlocks(
157297export class TestRunnerCodeLensProvider implements CodeLensProvider {
158298 private lastSuccessfulCodeLens : Map < string , CodeLens [ ] > = new Map ( ) ;
159299
160- constructor ( private readonly codeLensOptions : CodeLensOption [ ] ) { }
300+ constructor ( private readonly codeLensOptions : CodeLensOption [ ] ) { }
161301
162302 public async provideCodeLenses ( document : TextDocument ) : Promise < CodeLens [ ] > {
163303 try {
164304 const workspaceFolder = workspace . getWorkspaceFolder ( document . uri ) ;
165305 const workspaceFolderPath = workspaceFolder ?. uri . fsPath ;
166- if ( ! workspaceFolderPath || ! testFileCache . isTestFile ( document . fileName ) ) {
306+ if (
307+ ! workspaceFolderPath ||
308+ ! testFileCache . isTestFile ( document . fileName )
309+ ) {
167310 return [ ] ;
168311 }
169312
170- const parseResults = parseTestFile ( document . fileName , document . getText ( ) ) . root . children ?? [ ] ;
313+ const parseResults =
314+ parseTestFile ( document . fileName , document . getText ( ) ) . root . children ??
315+ [ ] ;
171316
172317 const processedGroups = new Set < string > ( ) ;
173318
174319 const codeLenses = parseResults . flatMap ( ( parseResult ) =>
175- getTestsBlocks ( parseResult , parseResults , this . codeLensOptions , undefined , processedGroups ) ,
320+ getTestsBlocks (
321+ parseResult ,
322+ parseResults ,
323+ this . codeLensOptions ,
324+ undefined ,
325+ processedGroups ,
326+ ) ,
176327 ) ;
177328 this . lastSuccessfulCodeLens . set ( document . fileName , codeLenses ) ;
178329 return codeLenses ;
0 commit comments