Skip to content

Commit 6daff86

Browse files
authored
Support Jest v30 (#1153)
* support jest v30 syntax * adding tests * updating tests
1 parent 99724e9 commit 6daff86

File tree

10 files changed

+169
-20
lines changed

10 files changed

+169
-20
lines changed

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,12 @@
366366
"markdownDescription": "A detailed runMode configuration. See details in [runMode](https://github.com/jest-community/vscode-jest#runmode)"
367367
}
368368
]
369+
},
370+
"jest.useJest30": {
371+
"description": "Use Jest 30+ features",
372+
"type": "boolean",
373+
"default": null,
374+
"scope": "resource"
369375
}
370376
}
371377
},

src/JestExt/helper.ts

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export const getExtensionResourceSettings = (
107107
parserPluginOptions: getSetting<JESParserPluginOptions>('parserPluginOptions'),
108108
enable: getSetting<boolean>('enable'),
109109
useDashedArgs: getSetting<boolean>('useDashedArgs') ?? false,
110+
useJest30: getSetting<boolean>('useJest30'),
110111
};
111112
};
112113

src/JestExt/process-listeners.ts

+33-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
22
import { JestTotalResults, RunnerEvent } from 'jest-editor-support';
33
import { cleanAnsi, toErrorString } from '../helpers';
44
import { JestProcess, ProcessStatus } from '../JestProcessManagement';
5-
import { ListenerSession, ListTestFilesCallback } from './process-session';
5+
import { JestExtRequestType, ListenerSession, ListTestFilesCallback } from './process-session';
66
import { Logging } from '../logging';
77
import { JestRunEvent } from './types';
88
import { MonitorLongRun } from '../Settings';
@@ -12,6 +12,11 @@ import { RunShell } from './run-shell';
1212
// command not found error for anything but "jest", as it most likely not be caused by env issue
1313
const POSSIBLE_ENV_ERROR_REGEX =
1414
/^(((?!(jest|react-scripts)).)*)(command not found|no such file or directory)/im;
15+
16+
const TEST_PATH_PATTERNS_V30_ERROR_REGEX =
17+
/Option "testPathPattern" was replaced by "testPathPatterns"\./i;
18+
const TEST_PATH_PATTERNS_NOT_V30_ERROR_REGEX =
19+
/Unrecognized option "testPathPatterns". Did you mean "testPathPattern"\?/i;
1520
export class AbstractProcessListener {
1621
protected session: ListenerSession;
1722
protected readonly logging: Logging;
@@ -271,9 +276,31 @@ export class RunTestListener extends AbstractProcessListener {
271276
);
272277
return;
273278
}
274-
this.logging('debug', '--watch is not supported, will start the --watchAll run instead');
275-
this.session.scheduleProcess({ type: 'watch-all-tests' });
276-
process.stop();
279+
this.reScheduleProcess(
280+
process,
281+
'--watch is not supported, will start the --watchAll run instead',
282+
{ type: 'watch-all-tests' }
283+
);
284+
}
285+
}
286+
private reScheduleProcess(
287+
process: JestProcess,
288+
message: string,
289+
overrideRequest?: JestExtRequestType
290+
): void {
291+
this.logging('debug', message);
292+
this.session.context.output.write(`${message}\r\nReSchedule the process...`, 'warn');
293+
294+
this.session.scheduleProcess(overrideRequest ?? process.request);
295+
process.stop();
296+
}
297+
private handleTestPatternsError(process: JestProcess, data: string) {
298+
if (TEST_PATH_PATTERNS_V30_ERROR_REGEX.test(data)) {
299+
this.session.context.settings.useJest30 = true;
300+
this.reScheduleProcess(process, 'detected jest v30, enable useJest30 option');
301+
} else if (TEST_PATH_PATTERNS_NOT_V30_ERROR_REGEX.test(data)) {
302+
this.session.context.settings.useJest30 = false;
303+
this.reScheduleProcess(process, 'detected jest Not v30, disable useJest30 option');
277304
}
278305
}
279306

@@ -307,6 +334,8 @@ export class RunTestListener extends AbstractProcessListener {
307334
this.handleRunComplete(process, message);
308335

309336
this.handleWatchNotSupportedError(process, message);
337+
338+
this.handleTestPatternsError(process, message);
310339
}
311340

312341
private getNumTotalTestSuites(text: string): number | undefined {

src/JestProcessManagement/JestProcess.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ export class JestProcess implements JestProcessInfo {
122122
return `"${removeSurroundingQuote(aString)}"`;
123123
}
124124

125+
private getTestPathPattern(pattern: string): string[] {
126+
return this.extContext.settings.useJest30
127+
? ['--testPathPatterns', pattern]
128+
: ['--testPathPattern', pattern];
129+
}
125130
public start(): Promise<void> {
126131
if (this.status === ProcessStatus.Cancelled) {
127132
this.logging('warn', `the runner task has been cancelled!`);
@@ -166,7 +171,7 @@ export class JestProcess implements JestProcessInfo {
166171
}
167172
case 'by-file-pattern': {
168173
const regex = this.quoteFilePattern(escapeRegExp(this.request.testFileNamePattern));
169-
args.push('--watchAll=false', '--testPathPattern', regex);
174+
args.push('--watchAll=false', ...this.getTestPathPattern(regex));
170175
if (this.request.updateSnapshot) {
171176
args.push('--updateSnapshot');
172177
}
@@ -191,7 +196,7 @@ export class JestProcess implements JestProcessInfo {
191196
escapeRegExp(this.request.testNamePattern),
192197
this.extContext.settings.shell.toSetting()
193198
);
194-
args.push('--watchAll=false', '--testPathPattern', regex);
199+
args.push('--watchAll=false', ...this.getTestPathPattern(regex));
195200
if (this.request.updateSnapshot) {
196201
args.push('--updateSnapshot');
197202
}

src/Settings/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface PluginResourceSettings {
7777
enable?: boolean;
7878
parserPluginOptions?: JESParserPluginOptions;
7979
useDashedArgs?: boolean;
80+
useJest30?: boolean;
8081
}
8182

8283
export interface DeprecatedPluginResourceSettings {

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -355,20 +355,24 @@ export class WorkspaceRoot extends TestItemDataBase {
355355
return process.userData.testItem;
356356
}
357357

358-
// should only come here for autoRun processes
359358
let fileName;
360359
switch (process.request.type) {
361360
case 'watch-tests':
362361
case 'watch-all-tests':
363362
case 'all-tests':
364363
return this.item;
365364
case 'by-file':
365+
case 'by-file-test':
366366
fileName = process.request.testFileName;
367367
break;
368368
case 'by-file-pattern':
369+
case 'by-file-test-pattern':
369370
fileName = process.request.testFileNamePattern;
370371
break;
371372
default:
373+
// the current flow would not reach here, but for future proofing
374+
// and avoiding failed silently, we will keep the code around but disable coverage reporting
375+
/* istanbul ignore next */
372376
throw new Error(`unsupported external process type ${process.request.type}`);
373377
}
374378

@@ -404,8 +408,9 @@ export class WorkspaceRoot extends TestItemDataBase {
404408
return;
405409
}
406410

411+
let run;
407412
try {
408-
const run = this.getJestRun(event, true);
413+
run = this.getJestRun(event, true);
409414
switch (event.type) {
410415
case 'scheduled': {
411416
this.deepItemState(event.process.userData?.testItem, run.enqueued);
@@ -460,7 +465,8 @@ export class WorkspaceRoot extends TestItemDataBase {
460465
}
461466
} catch (err) {
462467
this.log('error', `<onRunEvent> ${event.type} failed:`, err);
463-
this.context.output.write(`<onRunEvent> ${event.type} failed: ${err}`, 'error');
468+
run?.write(`<onRunEvent> ${event.type} failed: ${err}`, 'error');
469+
run?.end({ reason: 'Internal error onRunEvent' });
464470
}
465471
};
466472

tests/JestExt/helper.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ describe('getExtensionResourceSettings()', () => {
177177
enable: true,
178178
nodeEnv: undefined,
179179
useDashedArgs: false,
180+
useJest30: null,
180181
});
181182
expect(createJestSettingGetter).toHaveBeenCalledWith(folder);
182183
});

tests/JestExt/process-listeners.test.ts

+44
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ describe('jest process listeners', () => {
5656
create: jest.fn(() => mockLogging),
5757
},
5858
onRunEvent: { fire: jest.fn() },
59+
output: { write: jest.fn() },
5960
},
6061
};
6162
mockProcess = initMockProcess('watch-tests');
@@ -240,6 +241,7 @@ describe('jest process listeners', () => {
240241
append: jest.fn(),
241242
clear: jest.fn(),
242243
show: jest.fn(),
244+
write: jest.fn(),
243245
};
244246
mockSession.context.updateWithData = jest.fn();
245247
});
@@ -575,5 +577,47 @@ describe('jest process listeners', () => {
575577
});
576578
});
577579
});
580+
describe('jest 30 support', () => {
581+
describe('can restart process if detected jest 30 related error', () => {
582+
it.each`
583+
case | output | useJest30Before | useJest30After | willRestart
584+
${1} | ${'Error in JestTestPatterns'} | ${null} | ${null} | ${false}
585+
${2} | ${'Error in JestTestPatterns'} | ${true} | ${true} | ${false}
586+
${3} | ${'Process Failed\nOption "testPathPattern" was replaced by "testPathPatterns".'} | ${null} | ${true} | ${true}
587+
${4} | ${'Process Failed\nOption "testPathPattern" was replaced by "testPathPatterns".'} | ${false} | ${true} | ${true}
588+
`('case $case', ({ output, useJest30Before, useJest30After, willRestart }) => {
589+
expect.hasAssertions();
590+
mockSession.context.settings.useJest30 = useJest30Before;
591+
const listener = new RunTestListener(mockSession);
592+
593+
listener.onEvent(mockProcess, 'executableStdErr', Buffer.from(output));
594+
595+
expect(mockSession.context.settings.useJest30).toEqual(useJest30After);
596+
597+
if (willRestart) {
598+
expect(mockSession.scheduleProcess).toHaveBeenCalledTimes(1);
599+
expect(mockSession.scheduleProcess).toHaveBeenCalledWith(mockProcess.request);
600+
expect(mockProcess.stop).toHaveBeenCalled();
601+
} else {
602+
expect(mockSession.scheduleProcess).not.toHaveBeenCalled();
603+
expect(mockProcess.stop).not.toHaveBeenCalled();
604+
}
605+
});
606+
});
607+
it('can restart process if setting useJest30 for a non jest 30 runtime', () => {
608+
expect.hasAssertions();
609+
mockSession.context.settings.useJest30 = true;
610+
const listener = new RunTestListener(mockSession);
611+
612+
const output = `whatever\n Unrecognized option "testPathPatterns". Did you mean "testPathPattern"?\n`;
613+
listener.onEvent(mockProcess, 'executableStdErr', Buffer.from(output));
614+
615+
expect(mockSession.context.settings.useJest30).toEqual(false);
616+
617+
expect(mockSession.scheduleProcess).toHaveBeenCalledTimes(1);
618+
expect(mockSession.scheduleProcess).toHaveBeenCalledWith(mockProcess.request);
619+
expect(mockProcess.stop).toHaveBeenCalled();
620+
});
621+
});
578622
});
579623
});

tests/JestProcessManagement/JestProcess.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,28 @@ describe('JestProcess', () => {
186186
}
187187
);
188188
});
189+
describe('supports jest v30 options', () => {
190+
it.each`
191+
case | type | extraProperty | useJest30 | expectedOption
192+
${1} | ${'by-file-pattern'} | ${{ testFileNamePattern: 'abc' }} | ${null} | ${'--testPathPattern'}
193+
${2} | ${'by-file-pattern'} | ${{ testFileNamePattern: 'abc' }} | ${true} | ${'--testPathPatterns'}
194+
${3} | ${'by-file-pattern'} | ${{ testFileNamePattern: 'abc' }} | ${false} | ${'--testPathPattern'}
195+
${4} | ${'by-file-test-pattern'} | ${{ testFileNamePattern: 'abc', testNamePattern: 'abc' }} | ${null} | ${'--testPathPattern'}
196+
${5} | ${'by-file-test-pattern'} | ${{ testFileNamePattern: 'abc', testNamePattern: 'abc' }} | ${true} | ${'--testPathPatterns'}
197+
${6} | ${'by-file-test-pattern'} | ${{ testFileNamePattern: 'abc', testNamePattern: 'abc' }} | ${false} | ${'--testPathPattern'}
198+
`(
199+
'case $case: generate the correct TestPathPattern(s) option',
200+
({ type, extraProperty, useJest30, expectedOption }) => {
201+
expect.hasAssertions();
202+
extContext.settings.useJest30 = useJest30;
203+
const request = mockRequest(type, extraProperty);
204+
const jp = new JestProcess(extContext, request);
205+
jp.start();
206+
const [, options] = RunnerClassMock.mock.calls[0];
207+
expect(options.args.args).toContain(expectedOption);
208+
}
209+
);
210+
});
189211
describe('common flags', () => {
190212
it.each`
191213
type | extraProperty | excludeWatch | withColors

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

+45-11
Original file line numberDiff line numberDiff line change
@@ -1404,13 +1404,15 @@ describe('test-item-data', () => {
14041404
mockedJestTestRun.mockClear();
14051405
});
14061406
describe.each`
1407-
request | withFile
1408-
${{ type: 'watch-tests' }} | ${false}
1409-
${{ type: 'watch-all-tests' }} | ${false}
1410-
${{ type: 'all-tests' }} | ${false}
1411-
${{ type: 'by-file', testFileName: file }} | ${true}
1412-
${{ type: 'by-file', testFileName: 'source.ts', notTestFile: true }} | ${false}
1413-
${{ type: 'by-file-pattern', testFileNamePattern: file }} | ${true}
1407+
request | withFile
1408+
${{ type: 'watch-tests' }} | ${false}
1409+
${{ type: 'watch-all-tests' }} | ${false}
1410+
${{ type: 'all-tests' }} | ${false}
1411+
${{ type: 'by-file', testFileName: file }} | ${true}
1412+
${{ type: 'by-file', testFileName: 'source.ts', notTestFile: true }} | ${false}
1413+
${{ type: 'by-file-test', testFileName: file, testNamePattern: 'whatever' }} | ${true}
1414+
${{ type: 'by-file-pattern', testFileNamePattern: file }} | ${true}
1415+
${{ type: 'by-file-test-pattern', testFileNamePattern: file, testNamePattern: 'whatever' }} | ${true}
14141416
`('will create a new run and use it throughout: $request', ({ request, withFile }) => {
14151417
it('if only reports assertion-update, everything should still work', () => {
14161418
const process: any = { id: 'whatever', request };
@@ -1511,13 +1513,11 @@ describe('test-item-data', () => {
15111513
expect(process.userData.run.write).toHaveBeenCalledWith('whatever', 'error');
15121514
});
15131515
});
1514-
describe('request not supported', () => {
1516+
describe('on request not supported', () => {
15151517
it.each`
15161518
request
15171519
${{ type: 'not-test' }}
1518-
${{ type: 'by-file-test', testFileName: file, testNamePattern: 'whatever' }}
1519-
${{ type: 'by-file-test-pattern', testFileNamePattern: file, testNamePattern: 'whatever' }}
1520-
`('$request', ({ request }) => {
1520+
`('do nothing for request: $request', ({ request }) => {
15211521
const process = { id: 'whatever', request };
15221522

15231523
// starting the process
@@ -1557,6 +1557,40 @@ describe('test-item-data', () => {
15571557
errors.LONG_RUNNING_TESTS
15581558
);
15591559
});
1560+
describe('will catch runtime error and close the run', () => {
1561+
let process, jestRun;
1562+
beforeEach(() => {
1563+
process = mockScheduleProcess(context);
1564+
jestRun = createTestRun();
1565+
process.userData = { run: jestRun, testItem: env.testFile };
1566+
});
1567+
1568+
it('when run failed to be created', () => {
1569+
// simulate a runtime error
1570+
jestRun.addProcess = jest.fn(() => {
1571+
throw new Error('forced error');
1572+
});
1573+
// this will not throw error
1574+
env.onRunEvent({ type: 'start', process });
1575+
1576+
expect(jestRun.started).toHaveBeenCalledTimes(0);
1577+
expect(jestRun.end).toHaveBeenCalledTimes(0);
1578+
expect(jestRun.write).toHaveBeenCalledTimes(0);
1579+
});
1580+
it('when run is created', () => {
1581+
// simulate a runtime error
1582+
jestRun.started = jest.fn(() => {
1583+
throw new Error('forced error');
1584+
});
1585+
1586+
// this will not throw error
1587+
env.onRunEvent({ type: 'start', process });
1588+
1589+
expect(jestRun.started).toHaveBeenCalledTimes(1);
1590+
expect(jestRun.end).toHaveBeenCalledTimes(1);
1591+
expect(jestRun.write).toHaveBeenCalledTimes(1);
1592+
});
1593+
});
15601594
});
15611595
});
15621596
describe('createTestItem', () => {

0 commit comments

Comments
 (0)