Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Jest v30 #1153

Merged
merged 3 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading