Skip to content

Commit 5557e93

Browse files
authored
use exact regex for TestNamePattern if the item is a test block. (#1091)
1 parent 66c5d56 commit 5557e93

File tree

11 files changed

+149
-260
lines changed

11 files changed

+149
-260
lines changed

src/JestExt/core.ts

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,17 @@ import {
88
SortedTestResults,
99
TestResultProviderOptions,
1010
} from '../TestResults';
11-
import {
12-
testIdString,
13-
IdStringType,
14-
escapeRegExp,
15-
emptyTestStats,
16-
getValidJestCommand,
17-
} from '../helpers';
11+
import { escapeRegExp, emptyTestStats, getValidJestCommand } from '../helpers';
1812
import { CoverageMapProvider, CoverageCodeLensProvider } from '../Coverage';
1913
import { updateDiagnostics, updateCurrentDiagnostics, resetDiagnostics } from '../diagnostics';
2014
import { DebugConfigurationProvider } from '../DebugConfigurationProvider';
21-
import { TestExplorerRunRequest, TestStats } from '../types';
15+
import { TestExplorerRunRequest, TestNamePattern, TestStats } from '../types';
2216
import { CoverageOverlay } from '../Coverage/CoverageOverlay';
2317
import { resultsWithoutAnsiEscapeSequence } from '../TestResults/TestResult';
2418
import { CoverageMapData } from 'istanbul-lib-coverage';
2519
import { Logging } from '../logging';
2620
import { createProcessSession, ProcessSession } from './process-session';
27-
import {
28-
JestExtContext,
29-
JestSessionEvents,
30-
JestExtSessionContext,
31-
JestRunEvent,
32-
DebugTestIdentifier,
33-
} from './types';
21+
import { JestExtContext, JestSessionEvents, JestExtSessionContext, JestRunEvent } from './types';
3422
import { extensionName, SupportedLanguageIds } from '../appGlobals';
3523
import { createJestExtContext, getExtensionResourceSettings, prefixWorkspace } from './helper';
3624
import { PluginResourceSettings } from '../Settings';
@@ -45,10 +33,6 @@ import { QuickFixActionType } from '../quick-fix';
4533
import { executableTerminalLinkProvider } from '../terminal-link-provider';
4634
import { outputManager } from '../output-manager';
4735

48-
interface RunTestPickItem extends vscode.QuickPickItem {
49-
id: DebugTestIdentifier;
50-
}
51-
5236
interface JestCommandSettings {
5337
rootPath: string;
5438
jestCommandLine: string;
@@ -562,10 +546,8 @@ export class JestExt {
562546
//** commands */
563547
public debugTests = async (
564548
document: vscode.TextDocument | string,
565-
...ids: DebugTestIdentifier[]
549+
testNamePattern?: TestNamePattern
566550
): Promise<void> => {
567-
const idString = (type: IdStringType, id: DebugTestIdentifier): string =>
568-
typeof id === 'string' ? id : testIdString(type, id);
569551
const getDebugConfig = (
570552
folder?: vscode.WorkspaceFolder
571553
): vscode.DebugConfiguration | undefined => {
@@ -586,39 +568,10 @@ export class JestExt {
586568
}
587569
}
588570
};
589-
const selectTest = async (
590-
testIdentifiers: DebugTestIdentifier[]
591-
): Promise<DebugTestIdentifier | undefined> => {
592-
const items: RunTestPickItem[] = testIdentifiers.map((id) => ({
593-
label: idString('display-reverse', id),
594-
id,
595-
}));
596-
const selected = await vscode.window.showQuickPick<RunTestPickItem>(items, {
597-
placeHolder: 'Select a test to debug',
598-
});
599-
600-
return selected?.id;
601-
};
602-
let testId: DebugTestIdentifier | undefined;
603-
switch (ids.length) {
604-
case 0:
605-
//no testId, will run all tests in the file
606-
break;
607-
case 1:
608-
testId = ids[0];
609-
break;
610-
default:
611-
testId = await selectTest(ids);
612-
// if nothing is selected, abort
613-
if (!testId) {
614-
return;
615-
}
616-
break;
617-
}
618571

619572
this.debugConfigurationProvider.prepareTestRun(
620573
typeof document === 'string' ? document : document.fileName,
621-
testId ? escapeRegExp(idString('full-name', testId)) : '.*',
574+
testNamePattern ? escapeRegExp(testNamePattern) : '.*',
622575
this.extContext.workspace
623576
);
624577

src/JestExt/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ProcessSession } from './process-session';
77
import { JestProcessInfo } from '../JestProcessManagement';
88
import { JestOutputTerminal } from './output-terminal';
99
import { TestIdentifier } from '../TestResults';
10+
import { TestNamePattern } from '../types';
1011

1112
export enum WatchMode {
1213
None = 'none',
@@ -56,5 +57,5 @@ export type JestExtProcessContext = Readonly<JestExtProcessContextRaw>;
5657
export type DebugTestIdentifier = string | TestIdentifier;
5758
export type DebugFunction = (
5859
document: vscode.TextDocument | string,
59-
...ids: DebugTestIdentifier[]
60+
testNamePattern?: TestNamePattern
6061
) => Promise<void>;

src/JestProcessManagement/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { RunnerEvent } from 'jest-editor-support';
33
import { JestTestProcessType } from '../Settings';
44
import { JestProcess } from './JestProcess';
55
import { JestTestRun } from '../test-provider/jest-test-run';
6+
import { TestNamePattern } from '../types';
67

78
export interface JestProcessListener {
89
onEvent: (process: JestProcess, event: RunnerEvent, ...args: unknown[]) => unknown;
@@ -71,7 +72,7 @@ export type JestProcessRequestSimple =
7172
| {
7273
type: Extract<JestTestProcessType, 'by-file-test'>;
7374
testFileName: string;
74-
testNamePattern: string;
75+
testNamePattern: TestNamePattern;
7576
updateSnapshot?: boolean;
7677
}
7778
| {
@@ -82,7 +83,7 @@ export type JestProcessRequestSimple =
8283
| {
8384
type: Extract<JestTestProcessType, 'by-file-test-pattern'>;
8485
testFileNamePattern: string;
85-
testNamePattern: string;
86+
testNamePattern: TestNamePattern;
8687
updateSnapshot?: boolean;
8788
}
8889
| {

src/TestResults/snapshot-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class SnapshotProvider {
5050
public async previewSnapshot(testPath: string, testFullName: string): Promise<void> {
5151
const content = await this.snapshotSupport.getSnapshotContent(
5252
testPath,
53-
new RegExp(`^${escapeRegExp(testFullName)} [0-9]+$`)
53+
new RegExp(`^${escapeRegExp({ value: testFullName, exactMatch: false })} [0-9]+$`)
5454
);
5555
const noSnapshotFound = (): void => {
5656
vscode.window.showErrorMessage('no snapshot is found, please run test to generate first');

src/helpers.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { join, resolve, normalize, isAbsolute } from 'path';
55
import { ExtensionContext } from 'vscode';
66

77
import { TestIdentifier } from './TestResults';
8-
import { TestStats } from './types';
8+
import { StringPattern, TestStats } from './types';
99
import { LoginShell } from 'jest-editor-support';
1010
import { WorkspaceManager } from './workspace-manager';
1111

@@ -125,10 +125,22 @@ export const getDefaultJestCommand = (rootPath = ''): string | undefined => {
125125
};
126126

127127
/**
128-
* Taken From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
128+
* Escapes special characters in a string to be used as a regular expression pattern.
129+
* @param str - The string to escape.
130+
* @returns The escaped string.
131+
*
132+
* Note: the conversion algorithm is taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
129133
*/
130-
export function escapeRegExp(str: string): string {
131-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
134+
export function escapeRegExp(str: string | StringPattern): string {
135+
const sp: StringPattern = typeof str === 'string' ? { value: str } : str;
136+
if (sp.isRegExp) {
137+
return sp.value;
138+
}
139+
const escaped = sp.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
140+
if (sp.exactMatch) {
141+
return escaped + '$';
142+
}
143+
return escaped;
132144
}
133145

134146
/**

src/test-provider/test-item-data.ts

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import { JestTestProviderContext } from './test-provider-context';
1313
import { JestTestRun } from './jest-test-run';
1414
import { JestProcessInfo, JestProcessRequest } from '../JestProcessManagement';
1515
import { GENERIC_ERROR, LONG_RUNNING_TESTS, getExitErrorDef } from '../errors';
16-
import { JestExtOutput } from '../JestExt/output-terminal';
1716
import { tiContextManager } from './test-item-context-manager';
1817
import { toAbsoluteRootPath } from '../helpers';
1918
import { runModeDescription } from '../JestExt/run-mode';
2019
import { isVirtualWorkspaceFolder } from '../virtual-workspace-folder';
2120
import { outputManager } from '../output-manager';
21+
import { TestNamePattern } from '../types';
2222

2323
interface JestRunnable {
2424
getJestRunRequest: () => JestExtRequestType;
@@ -370,6 +370,7 @@ export class WorkspaceRoot extends TestItemDataBase {
370370
/** return a valid run from event. if createIfMissing is true, then create a new one if none exist in the event **/
371371
private getJestRun(event: TypedRunEvent, createIfMissing: true): JestTestRun;
372372
private getJestRun(event: TypedRunEvent, createIfMissing?: false): JestTestRun | undefined;
373+
// istanbul ignore next
373374
private getJestRun(event: TypedRunEvent, createIfMissing = false): JestTestRun | undefined {
374375
if (event.process.userData?.run) {
375376
return event.process.userData.run;
@@ -393,9 +394,6 @@ export class WorkspaceRoot extends TestItemDataBase {
393394
'new-line',
394395
]);
395396
}
396-
private writer(run?: JestTestRun): JestExtOutput {
397-
return run ?? this.context.output;
398-
}
399397
private onRunEvent = (event: JestRunEvent) => {
400398
if (event.process.request.type === 'not-test') {
401399
return;
@@ -412,7 +410,7 @@ export class WorkspaceRoot extends TestItemDataBase {
412410
const text = event.raw ?? event.text;
413411
if (text && text.length > 0) {
414412
const opt = event.isError ? 'error' : event.newLine ? 'new-line' : undefined;
415-
this.writer(run).write(text, opt);
413+
run.write(text, opt);
416414
}
417415
break;
418416
}
@@ -424,11 +422,11 @@ export class WorkspaceRoot extends TestItemDataBase {
424422
}
425423
case 'end': {
426424
if (event.error && !event.process.userData?.execError) {
427-
this.writer(run).write(event.error, 'error');
425+
run.write(event.error, 'error');
428426
event.process.userData = { ...(event.process.userData ?? {}), execError: true };
429427
}
430428
this.runLog('finished');
431-
run?.end({ pid: event.process.id, delay: 30000, reason: 'process end' });
429+
run.end({ pid: event.process.id, delay: 30000, reason: 'process end' });
432430
break;
433431
}
434432
case 'exit': {
@@ -448,7 +446,7 @@ export class WorkspaceRoot extends TestItemDataBase {
448446
break;
449447
}
450448
case 'long-run': {
451-
this.writer(run).write(
449+
run.write(
452450
`Long Running Tests Warning: Tests exceeds ${event.threshold}ms threshold. Please reference Troubleshooting if this is not expected`,
453451
LONG_RUNNING_TESTS
454452
);
@@ -505,13 +503,10 @@ const isAssertDataNode = (arg: ItemNodeType): arg is DataNode<TestAssertionStatu
505503
// eslint-disable-next-line @typescript-eslint/no-explicit-any
506504
isDataNode(arg) && (arg.data as any).fullName;
507505

508-
const isEmpty = (node?: ItemNodeType): boolean => {
506+
const isContainerEmpty = (node?: ContainerNode<TestAssertionStatus>): boolean => {
509507
if (!node) {
510508
return true;
511509
}
512-
if (isDataNode(node)) {
513-
return false;
514-
}
515510
if (
516511
(node.childData && node.childData.length > 0) ||
517512
(node.childContainers && node.childContainers.length > 0)
@@ -570,21 +565,12 @@ abstract class TestResultData extends TestItemDataBase {
570565
return parts.join('#');
571566
}
572567

573-
isSameId(id1: string, id2: string): boolean {
574-
if (id1 === id2) {
575-
return true;
576-
}
577-
// truncate the last "extra-id" added for duplicate test names before comparing
578-
const truncateExtra = (id: string): string => id.replace(/(.*)(#[0-9]+$)/, '$1');
579-
return truncateExtra(id1) === truncateExtra(id2);
580-
}
581-
568+
/**
569+
* Synchronizes the child nodes of the test item with the given ItemNodeType, recursively.
570+
* @param node - The ItemNodeType to synchronize the child nodes with.
571+
* @returns void
572+
*/
582573
syncChildNodes(node: ItemNodeType): void {
583-
const testId = this.makeTestId(this.uri, node);
584-
if (!this.isSameId(testId, this.item.id)) {
585-
this.item.error = 'invalid node';
586-
return;
587-
}
588574
this.item.error = undefined;
589575

590576
if (!isDataNode(node)) {
@@ -684,7 +670,7 @@ export class TestDocumentRoot extends TestResultData {
684670
// test file has syntax error or failed to run for whatever reason.
685671
// In this case we should mark the suite itself as TestExplorer won't be able to
686672
// aggregate from the children list
687-
if (isEmpty(suiteResult?.assertionContainer)) {
673+
if (isContainerEmpty(suiteResult?.assertionContainer)) {
688674
this.updateItemState(run, suiteResult);
689675
}
690676
this.forEachChild((child) => child.updateResultState(run));
@@ -736,17 +722,24 @@ export class TestData extends TestResultData implements Debuggable {
736722
return item;
737723
}
738724

725+
private getTestNamePattern(): TestNamePattern {
726+
if (isDataNode(this.node)) {
727+
return { value: this.node.fullName, exactMatch: true };
728+
}
729+
return { value: this.node.fullName, exactMatch: false };
730+
}
731+
739732
getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType {
740733
return {
741734
type: 'by-file-test-pattern',
742735
updateSnapshot: itemCommand === ItemCommand.updateSnapshot,
743736
testFileNamePattern: this.uri.fsPath,
744-
testNamePattern: this.node.fullName,
737+
testNamePattern: this.getTestNamePattern(),
745738
};
746739
}
747740

748741
getDebugInfo(): ReturnType<Debuggable['getDebugInfo']> {
749-
return { fileName: this.uri.fsPath, testNamePattern: this.node.fullName };
742+
return { fileName: this.uri.fsPath, testNamePattern: this.getTestNamePattern() };
750743
}
751744
private updateItemRange(): void {
752745
if (this.node.attrs.range) {

src/test-provider/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TestResultProvider } from '../TestResults';
44
import { WorkspaceRoot, FolderData, TestData, TestDocumentRoot } from './test-item-data';
55
import { JestTestProviderContext } from './test-provider-context';
66
import { JestTestRun } from './jest-test-run';
7+
import { TestNamePattern } from '../types';
78

89
export type TestItemDataType = WorkspaceRoot | FolderData | TestDocumentRoot | TestData;
910

@@ -24,7 +25,7 @@ export interface TestItemData {
2425
}
2526

2627
export interface Debuggable {
27-
getDebugInfo: () => { fileName: string; testNamePattern?: string };
28+
getDebugInfo: () => { fileName: string; testNamePattern?: TestNamePattern };
2829
}
2930

3031
export enum TestTagId {

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ export interface TestExplorerRunRequest {
1515
request: vscode.TestRunRequest;
1616
token: vscode.CancellationToken;
1717
}
18+
19+
export interface StringPattern {
20+
value: string;
21+
exactMatch?: boolean;
22+
isRegExp?: boolean;
23+
}
24+
25+
export type TestNamePattern = StringPattern | string;

0 commit comments

Comments
 (0)