Skip to content

Commit b8891cd

Browse files
authored
Fixes for Descirbe.each (#470)
* describe.each * refactor: streamline test item creation and folder handling in TestFileWatcher and testDiscovery * formatting * fix: resolve test name string interpolation in toTestItemNamePattern function * refactor: improve code readability in provideCodeLenses test
1 parent fcc5b58 commit b8891cd

13 files changed

Lines changed: 2705 additions & 1508 deletions

src/TestRunnerCodeLensProvider.ts

Lines changed: 212 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ import {
88
} from 'vscode';
99
import { testFileCache } from './testDetection/testFileCache';
1010
import { CodeLensOption } from './util';
11-
import { escapeRegExp, findFullTestName, resolveTestNameStringInterpolation, TestNode } from './utils/TestNameUtils';
11+
import {
12+
findFullTestName,
13+
toTestNamePattern,
14+
TestNode,
15+
} from './utils/TestNameUtils';
1216
import { logError } from './utils/Logger';
1317

18+
type LensNode = TestNode & { eachTemplate?: string; children?: LensNode[] };
19+
1420
const 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+
77212
function 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(
157297
export 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

Comments
 (0)