Skip to content

Commit

Permalink
Support Jest v30 (#1153)
Browse files Browse the repository at this point in the history
* support jest v30 syntax

* adding tests

* updating tests
  • Loading branch information
connectdotz authored Aug 8, 2024
1 parent 99724e9 commit 6daff86
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 20 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,12 @@
"markdownDescription": "A detailed runMode configuration. See details in [runMode](https://github.com/jest-community/vscode-jest#runmode)"
}
]
},
"jest.useJest30": {
"description": "Use Jest 30+ features",
"type": "boolean",
"default": null,
"scope": "resource"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions src/JestExt/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const getExtensionResourceSettings = (
parserPluginOptions: getSetting<JESParserPluginOptions>('parserPluginOptions'),
enable: getSetting<boolean>('enable'),
useDashedArgs: getSetting<boolean>('useDashedArgs') ?? false,
useJest30: getSetting<boolean>('useJest30'),
};
};

Expand Down
37 changes: 33 additions & 4 deletions src/JestExt/process-listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as vscode from 'vscode';
import { JestTotalResults, RunnerEvent } from 'jest-editor-support';
import { cleanAnsi, toErrorString } from '../helpers';
import { JestProcess, ProcessStatus } from '../JestProcessManagement';
import { ListenerSession, ListTestFilesCallback } from './process-session';
import { JestExtRequestType, ListenerSession, ListTestFilesCallback } from './process-session';
import { Logging } from '../logging';
import { JestRunEvent } from './types';
import { MonitorLongRun } from '../Settings';
Expand All @@ -12,6 +12,11 @@ import { RunShell } from './run-shell';
// command not found error for anything but "jest", as it most likely not be caused by env issue
const POSSIBLE_ENV_ERROR_REGEX =
/^(((?!(jest|react-scripts)).)*)(command not found|no such file or directory)/im;

const TEST_PATH_PATTERNS_V30_ERROR_REGEX =
/Option "testPathPattern" was replaced by "testPathPatterns"\./i;
const TEST_PATH_PATTERNS_NOT_V30_ERROR_REGEX =
/Unrecognized option "testPathPatterns". Did you mean "testPathPattern"\?/i;
export class AbstractProcessListener {
protected session: ListenerSession;
protected readonly logging: Logging;
Expand Down Expand Up @@ -271,9 +276,31 @@ export class RunTestListener extends AbstractProcessListener {
);
return;
}
this.logging('debug', '--watch is not supported, will start the --watchAll run instead');
this.session.scheduleProcess({ type: 'watch-all-tests' });
process.stop();
this.reScheduleProcess(
process,
'--watch is not supported, will start the --watchAll run instead',
{ type: 'watch-all-tests' }
);
}
}
private reScheduleProcess(
process: JestProcess,
message: string,
overrideRequest?: JestExtRequestType
): void {
this.logging('debug', message);
this.session.context.output.write(`${message}\r\nReSchedule the process...`, 'warn');

this.session.scheduleProcess(overrideRequest ?? process.request);
process.stop();
}
private handleTestPatternsError(process: JestProcess, data: string) {
if (TEST_PATH_PATTERNS_V30_ERROR_REGEX.test(data)) {
this.session.context.settings.useJest30 = true;
this.reScheduleProcess(process, 'detected jest v30, enable useJest30 option');
} else if (TEST_PATH_PATTERNS_NOT_V30_ERROR_REGEX.test(data)) {
this.session.context.settings.useJest30 = false;
this.reScheduleProcess(process, 'detected jest Not v30, disable useJest30 option');
}
}

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

this.handleWatchNotSupportedError(process, message);

this.handleTestPatternsError(process, message);
}

private getNumTotalTestSuites(text: string): number | undefined {
Expand Down
9 changes: 7 additions & 2 deletions src/JestProcessManagement/JestProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ export class JestProcess implements JestProcessInfo {
return `"${removeSurroundingQuote(aString)}"`;
}

private getTestPathPattern(pattern: string): string[] {
return this.extContext.settings.useJest30
? ['--testPathPatterns', pattern]
: ['--testPathPattern', pattern];
}
public start(): Promise<void> {
if (this.status === ProcessStatus.Cancelled) {
this.logging('warn', `the runner task has been cancelled!`);
Expand Down Expand Up @@ -166,7 +171,7 @@ export class JestProcess implements JestProcessInfo {
}
case 'by-file-pattern': {
const regex = this.quoteFilePattern(escapeRegExp(this.request.testFileNamePattern));
args.push('--watchAll=false', '--testPathPattern', regex);
args.push('--watchAll=false', ...this.getTestPathPattern(regex));
if (this.request.updateSnapshot) {
args.push('--updateSnapshot');
}
Expand All @@ -191,7 +196,7 @@ export class JestProcess implements JestProcessInfo {
escapeRegExp(this.request.testNamePattern),
this.extContext.settings.shell.toSetting()
);
args.push('--watchAll=false', '--testPathPattern', regex);
args.push('--watchAll=false', ...this.getTestPathPattern(regex));
if (this.request.updateSnapshot) {
args.push('--updateSnapshot');
}
Expand Down
1 change: 1 addition & 0 deletions src/Settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface PluginResourceSettings {
enable?: boolean;
parserPluginOptions?: JESParserPluginOptions;
useDashedArgs?: boolean;
useJest30?: boolean;
}

export interface DeprecatedPluginResourceSettings {
Expand Down
12 changes: 9 additions & 3 deletions src/test-provider/test-item-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,20 +355,24 @@ export class WorkspaceRoot extends TestItemDataBase {
return process.userData.testItem;
}

// should only come here for autoRun processes
let fileName;
switch (process.request.type) {
case 'watch-tests':
case 'watch-all-tests':
case 'all-tests':
return this.item;
case 'by-file':
case 'by-file-test':
fileName = process.request.testFileName;
break;
case 'by-file-pattern':
case 'by-file-test-pattern':
fileName = process.request.testFileNamePattern;
break;
default:
// the current flow would not reach here, but for future proofing
// and avoiding failed silently, we will keep the code around but disable coverage reporting
/* istanbul ignore next */
throw new Error(`unsupported external process type ${process.request.type}`);
}

Expand Down Expand Up @@ -404,8 +408,9 @@ export class WorkspaceRoot extends TestItemDataBase {
return;
}

let run;
try {
const run = this.getJestRun(event, true);
run = this.getJestRun(event, true);
switch (event.type) {
case 'scheduled': {
this.deepItemState(event.process.userData?.testItem, run.enqueued);
Expand Down Expand Up @@ -460,7 +465,8 @@ export class WorkspaceRoot extends TestItemDataBase {
}
} catch (err) {
this.log('error', `<onRunEvent> ${event.type} failed:`, err);
this.context.output.write(`<onRunEvent> ${event.type} failed: ${err}`, 'error');
run?.write(`<onRunEvent> ${event.type} failed: ${err}`, 'error');
run?.end({ reason: 'Internal error onRunEvent' });
}
};

Expand Down
1 change: 1 addition & 0 deletions tests/JestExt/helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ describe('getExtensionResourceSettings()', () => {
enable: true,
nodeEnv: undefined,
useDashedArgs: false,
useJest30: null,
});
expect(createJestSettingGetter).toHaveBeenCalledWith(folder);
});
Expand Down
44 changes: 44 additions & 0 deletions tests/JestExt/process-listeners.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('jest process listeners', () => {
create: jest.fn(() => mockLogging),
},
onRunEvent: { fire: jest.fn() },
output: { write: jest.fn() },
},
};
mockProcess = initMockProcess('watch-tests');
Expand Down Expand Up @@ -240,6 +241,7 @@ describe('jest process listeners', () => {
append: jest.fn(),
clear: jest.fn(),
show: jest.fn(),
write: jest.fn(),
};
mockSession.context.updateWithData = jest.fn();
});
Expand Down Expand Up @@ -575,5 +577,47 @@ describe('jest process listeners', () => {
});
});
});
describe('jest 30 support', () => {
describe('can restart process if detected jest 30 related error', () => {
it.each`
case | output | useJest30Before | useJest30After | willRestart
${1} | ${'Error in JestTestPatterns'} | ${null} | ${null} | ${false}
${2} | ${'Error in JestTestPatterns'} | ${true} | ${true} | ${false}
${3} | ${'Process Failed\nOption "testPathPattern" was replaced by "testPathPatterns".'} | ${null} | ${true} | ${true}
${4} | ${'Process Failed\nOption "testPathPattern" was replaced by "testPathPatterns".'} | ${false} | ${true} | ${true}
`('case $case', ({ output, useJest30Before, useJest30After, willRestart }) => {
expect.hasAssertions();
mockSession.context.settings.useJest30 = useJest30Before;
const listener = new RunTestListener(mockSession);

listener.onEvent(mockProcess, 'executableStdErr', Buffer.from(output));

expect(mockSession.context.settings.useJest30).toEqual(useJest30After);

if (willRestart) {
expect(mockSession.scheduleProcess).toHaveBeenCalledTimes(1);
expect(mockSession.scheduleProcess).toHaveBeenCalledWith(mockProcess.request);
expect(mockProcess.stop).toHaveBeenCalled();
} else {
expect(mockSession.scheduleProcess).not.toHaveBeenCalled();
expect(mockProcess.stop).not.toHaveBeenCalled();
}
});
});
it('can restart process if setting useJest30 for a non jest 30 runtime', () => {
expect.hasAssertions();
mockSession.context.settings.useJest30 = true;
const listener = new RunTestListener(mockSession);

const output = `whatever\n Unrecognized option "testPathPatterns". Did you mean "testPathPattern"?\n`;
listener.onEvent(mockProcess, 'executableStdErr', Buffer.from(output));

expect(mockSession.context.settings.useJest30).toEqual(false);

expect(mockSession.scheduleProcess).toHaveBeenCalledTimes(1);
expect(mockSession.scheduleProcess).toHaveBeenCalledWith(mockProcess.request);
expect(mockProcess.stop).toHaveBeenCalled();
});
});
});
});
22 changes: 22 additions & 0 deletions tests/JestProcessManagement/JestProcess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,28 @@ describe('JestProcess', () => {
}
);
});
describe('supports jest v30 options', () => {
it.each`
case | type | extraProperty | useJest30 | expectedOption
${1} | ${'by-file-pattern'} | ${{ testFileNamePattern: 'abc' }} | ${null} | ${'--testPathPattern'}
${2} | ${'by-file-pattern'} | ${{ testFileNamePattern: 'abc' }} | ${true} | ${'--testPathPatterns'}
${3} | ${'by-file-pattern'} | ${{ testFileNamePattern: 'abc' }} | ${false} | ${'--testPathPattern'}
${4} | ${'by-file-test-pattern'} | ${{ testFileNamePattern: 'abc', testNamePattern: 'abc' }} | ${null} | ${'--testPathPattern'}
${5} | ${'by-file-test-pattern'} | ${{ testFileNamePattern: 'abc', testNamePattern: 'abc' }} | ${true} | ${'--testPathPatterns'}
${6} | ${'by-file-test-pattern'} | ${{ testFileNamePattern: 'abc', testNamePattern: 'abc' }} | ${false} | ${'--testPathPattern'}
`(
'case $case: generate the correct TestPathPattern(s) option',
({ type, extraProperty, useJest30, expectedOption }) => {
expect.hasAssertions();
extContext.settings.useJest30 = useJest30;
const request = mockRequest(type, extraProperty);
const jp = new JestProcess(extContext, request);
jp.start();
const [, options] = RunnerClassMock.mock.calls[0];
expect(options.args.args).toContain(expectedOption);
}
);
});
describe('common flags', () => {
it.each`
type | extraProperty | excludeWatch | withColors
Expand Down
56 changes: 45 additions & 11 deletions tests/test-provider/test-item-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1404,13 +1404,15 @@ describe('test-item-data', () => {
mockedJestTestRun.mockClear();
});
describe.each`
request | withFile
${{ type: 'watch-tests' }} | ${false}
${{ type: 'watch-all-tests' }} | ${false}
${{ type: 'all-tests' }} | ${false}
${{ type: 'by-file', testFileName: file }} | ${true}
${{ type: 'by-file', testFileName: 'source.ts', notTestFile: true }} | ${false}
${{ type: 'by-file-pattern', testFileNamePattern: file }} | ${true}
request | withFile
${{ type: 'watch-tests' }} | ${false}
${{ type: 'watch-all-tests' }} | ${false}
${{ type: 'all-tests' }} | ${false}
${{ type: 'by-file', testFileName: file }} | ${true}
${{ type: 'by-file', testFileName: 'source.ts', notTestFile: true }} | ${false}
${{ type: 'by-file-test', testFileName: file, testNamePattern: 'whatever' }} | ${true}
${{ type: 'by-file-pattern', testFileNamePattern: file }} | ${true}
${{ type: 'by-file-test-pattern', testFileNamePattern: file, testNamePattern: 'whatever' }} | ${true}
`('will create a new run and use it throughout: $request', ({ request, withFile }) => {
it('if only reports assertion-update, everything should still work', () => {
const process: any = { id: 'whatever', request };
Expand Down Expand Up @@ -1511,13 +1513,11 @@ describe('test-item-data', () => {
expect(process.userData.run.write).toHaveBeenCalledWith('whatever', 'error');
});
});
describe('request not supported', () => {
describe('on request not supported', () => {
it.each`
request
${{ type: 'not-test' }}
${{ type: 'by-file-test', testFileName: file, testNamePattern: 'whatever' }}
${{ type: 'by-file-test-pattern', testFileNamePattern: file, testNamePattern: 'whatever' }}
`('$request', ({ request }) => {
`('do nothing for request: $request', ({ request }) => {
const process = { id: 'whatever', request };

// starting the process
Expand Down Expand Up @@ -1557,6 +1557,40 @@ describe('test-item-data', () => {
errors.LONG_RUNNING_TESTS
);
});
describe('will catch runtime error and close the run', () => {
let process, jestRun;
beforeEach(() => {
process = mockScheduleProcess(context);
jestRun = createTestRun();
process.userData = { run: jestRun, testItem: env.testFile };
});

it('when run failed to be created', () => {
// simulate a runtime error
jestRun.addProcess = jest.fn(() => {
throw new Error('forced error');
});
// this will not throw error
env.onRunEvent({ type: 'start', process });

expect(jestRun.started).toHaveBeenCalledTimes(0);
expect(jestRun.end).toHaveBeenCalledTimes(0);
expect(jestRun.write).toHaveBeenCalledTimes(0);
});
it('when run is created', () => {
// simulate a runtime error
jestRun.started = jest.fn(() => {
throw new Error('forced error');
});

// this will not throw error
env.onRunEvent({ type: 'start', process });

expect(jestRun.started).toHaveBeenCalledTimes(1);
expect(jestRun.end).toHaveBeenCalledTimes(1);
expect(jestRun.write).toHaveBeenCalledTimes(1);
});
});
});
});
describe('createTestItem', () => {
Expand Down

0 comments on commit 6daff86

Please sign in to comment.