diff --git a/e2e/test/coverage-analysis/jest-spec/jest.config.json b/e2e/test/coverage-analysis/jest-spec/jest.config.json index 18679b42cc..7dc4da8390 100644 --- a/e2e/test/coverage-analysis/jest-spec/jest.config.json +++ b/e2e/test/coverage-analysis/jest-spec/jest.config.json @@ -1,4 +1,5 @@ { "testEnvironment": "node", - "testMatch": ["**/jest-spec/*.spec.js"] + "rootDir": "..", + "testMatch": ["/jest-spec/*.spec.js"] } diff --git a/packages/jest-runner/src/config-loaders/custom-jest-config-loader.ts b/packages/jest-runner/src/config-loaders/custom-jest-config-loader.ts index 13bb2e7935..3b565304c8 100644 --- a/packages/jest-runner/src/config-loaders/custom-jest-config-loader.ts +++ b/packages/jest-runner/src/config-loaders/custom-jest-config-loader.ts @@ -51,7 +51,7 @@ export class CustomJestConfigLoader implements JestConfigLoader { private resolvePackageJsonFilePath(): string | undefined { const jestOptions = this.options as JestRunnerOptionsWithStrykerOptions; const packageJsonCandidate = path.resolve(jestOptions.jest.configFile ?? 'package.json'); - if (packageJsonCandidate.endsWith('.json') && (jestOptions.jest.configFile || fs.existsSync(packageJsonCandidate))) { + if (packageJsonCandidate.endsWith('package.json') && (jestOptions.jest.configFile || fs.existsSync(packageJsonCandidate))) { return packageJsonCandidate; } return undefined; @@ -63,7 +63,7 @@ export class CustomJestConfigLoader implements JestConfigLoader { private resolveJestConfigFilePath(): string | undefined { const jestOptions = this.options as JestRunnerOptionsWithStrykerOptions; const configFileCandidate = path.resolve(jestOptions.jest.configFile ?? 'jest.config.js'); - if (configFileCandidate.endsWith('.js') && (jestOptions.jest.configFile || fs.existsSync(configFileCandidate))) { + if (!configFileCandidate.endsWith('package.json') && (jestOptions.jest.configFile || fs.existsSync(configFileCandidate))) { return configFileCandidate; } return undefined; diff --git a/packages/jest-runner/src/jest-test-adapters/jest-greater-than-25-adapter.ts b/packages/jest-runner/src/jest-test-adapters/jest-greater-than-25-adapter.ts index b3f493ed94..b4ea5a2f9b 100644 --- a/packages/jest-runner/src/jest-test-adapters/jest-greater-than-25-adapter.ts +++ b/packages/jest-runner/src/jest-test-adapters/jest-greater-than-25-adapter.ts @@ -4,7 +4,7 @@ import { JestRunResult } from '../jest-run-result'; import { JestTestAdapter, RunSettings } from './jest-test-adapter'; export class JestGreaterThan25TestAdapter implements JestTestAdapter { - public async run({ jestConfig, projectRoot, fileNameUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise { + public async run({ jestConfig, fileNameUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise { const config = JSON.stringify(jestConfig); const result = await jestWrapper.runCLI( { @@ -17,7 +17,7 @@ export class JestGreaterThan25TestAdapter implements JestTestAdapter { testNamePattern, testLocationInResults, }, - [projectRoot] + [jestConfig.rootDir ?? process.cwd()] ); return result; } diff --git a/packages/jest-runner/src/jest-test-adapters/jest-less-than-25-adapter.ts b/packages/jest-runner/src/jest-test-adapters/jest-less-than-25-adapter.ts index 750eb5d1a1..9c792b35ec 100644 --- a/packages/jest-runner/src/jest-test-adapters/jest-less-than-25-adapter.ts +++ b/packages/jest-runner/src/jest-test-adapters/jest-less-than-25-adapter.ts @@ -1,5 +1,3 @@ -import { Config } from '@jest/types'; - import { jestWrapper } from '../utils'; import { JestRunResult } from '../jest-run-result'; @@ -10,17 +8,20 @@ import { RunSettings, JestTestAdapter } from './jest-test-adapter'; * It has a lot of `any` typings here, since the installed typings are not in sync. */ export class JestLessThan25TestAdapter implements JestTestAdapter { - public run({ jestConfig, projectRoot, fileNameUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise { + public run({ jestConfig, fileNameUnderTest, testNamePattern, testLocationInResults }: RunSettings): Promise { const config = JSON.stringify(jestConfig); return jestWrapper.runCLI( { - ...(fileNameUnderTest && { _: [fileNameUnderTest], findRelatedTests: true }), + $0: 'stryker', + _: fileNameUnderTest ? [fileNameUnderTest] : [], + findRelatedTests: !!fileNameUnderTest, config, runInBand: true, silent: true, testNamePattern, - } as Config.Argv, - [projectRoot] + testLocationInResults, + }, + [jestConfig.rootDir ?? process.cwd()] ); } } diff --git a/packages/jest-runner/src/jest-test-adapters/jest-test-adapter.ts b/packages/jest-runner/src/jest-test-adapters/jest-test-adapter.ts index 67b3e5f29b..a67c560188 100644 --- a/packages/jest-runner/src/jest-test-adapters/jest-test-adapter.ts +++ b/packages/jest-runner/src/jest-test-adapters/jest-test-adapter.ts @@ -4,7 +4,6 @@ import { JestRunResult } from '../jest-run-result'; export interface RunSettings { jestConfig: Config.InitialOptions; - projectRoot: string; testNamePattern?: string; fileNameUnderTest?: string; testLocationInResults?: boolean; diff --git a/packages/jest-runner/src/jest-test-runner.ts b/packages/jest-runner/src/jest-test-runner.ts index dead686c58..04e74f2853 100644 --- a/packages/jest-runner/src/jest-test-runner.ts +++ b/packages/jest-runner/src/jest-test-runner.ts @@ -106,7 +106,6 @@ export class JestTestRunner implements TestRunner { try { const { dryRunResult, jestResult } = await this.run({ jestConfig: withCoverageAnalysis(this.jestConfig, coverageAnalysis), - projectRoot: process.cwd(), testLocationInResults: true, }); if (dryRunResult.status === DryRunStatus.Complete && coverageAnalysis !== 'off') { @@ -138,7 +137,6 @@ export class JestTestRunner implements TestRunner { const { dryRunResult } = await this.run({ fileNameUnderTest, jestConfig: this.configForMutantRun(fileNameUnderTest), - projectRoot: process.cwd(), testNamePattern, }); return toMutantRunResult(dryRunResult); diff --git a/packages/jest-runner/src/plugin-tokens.ts b/packages/jest-runner/src/plugin-tokens.ts index 777b8d7cf6..b9076cb61d 100644 --- a/packages/jest-runner/src/plugin-tokens.ts +++ b/packages/jest-runner/src/plugin-tokens.ts @@ -1,5 +1,3 @@ -export const projectRoot = 'projectRoot'; -export const loader = 'loader'; export const resolve = 'resolve'; export const configLoader = 'configLoader'; export const processEnv = 'processEnv'; diff --git a/packages/jest-runner/src/utils/index.ts b/packages/jest-runner/src/utils/index.ts index 762120faf9..1b68b5b767 100644 --- a/packages/jest-runner/src/utils/index.ts +++ b/packages/jest-runner/src/utils/index.ts @@ -1,4 +1,3 @@ -export * from './create-react-jest-config'; export * from './verify-all-test-files-have-coverage'; export * from './merge-mutant-coverage'; export * from './jest-wrapper'; diff --git a/packages/jest-runner/test/integration/config-in-child-dir.it.spec.ts b/packages/jest-runner/test/integration/config-in-child-dir.it.spec.ts new file mode 100644 index 0000000000..9835c32cb8 --- /dev/null +++ b/packages/jest-runner/test/integration/config-in-child-dir.it.spec.ts @@ -0,0 +1,27 @@ +import { commonTokens } from '@stryker-mutator/api/plugin'; +import { assertions, factory, testInjector } from '@stryker-mutator/test-helpers'; +import { expect } from 'chai'; + +import { JestOptions } from '../../src-generated/jest-runner-options'; +import { JestRunnerOptionsWithStrykerOptions } from '../../src/jest-runner-options-with-stryker-options'; +import { jestTestRunnerFactory } from '../../src/jest-test-runner'; +import { createJestOptions } from '../helpers/producers'; + +import { resolveTestResource } from '../helpers/resolve-test-resource'; + +describe('jest with config in child dir', () => { + function createSut(overrides?: Partial) { + const options: JestRunnerOptionsWithStrykerOptions = factory.strykerWithPluginOptions({ + jest: createJestOptions(overrides), + }); + return testInjector.injector.provideValue(commonTokens.options, options).injectFunction(jestTestRunnerFactory); + } + + it('should still be able perform a mutant run', async () => { + process.chdir(resolveTestResource('config-in-child-dir')); + const sut = createSut({ configFile: 'client/jest.config.js' }); + const actual = await sut.dryRun(factory.dryRunOptions()); + assertions.expectCompleted(actual); + expect(actual.tests).lengthOf(2); + }); +}); diff --git a/packages/jest-runner/test/unit/config-loaders/custom-config-loader.spec.ts b/packages/jest-runner/test/unit/config-loaders/custom-config-loader.spec.ts index 8c6df70ff4..fff1797c16 100644 --- a/packages/jest-runner/test/unit/config-loaders/custom-config-loader.spec.ts +++ b/packages/jest-runner/test/unit/config-loaders/custom-config-loader.spec.ts @@ -57,6 +57,19 @@ describe(CustomJestConfigLoader.name, () => { expect(config).to.deep.contains({ rootDir: path.resolve(projectRoot, 'lib') }); }); + it('should allow users to configure a jest.config.json file as "configFile"', () => { + // Arrange + fileExistsSyncStub.returns(true); + options.jest.configFile = path.resolve(projectRoot, 'jest.config.json'); + + // Act + const config = sut.loadConfig(); + + // Assert + expect(requireStub).calledWith(path.join(projectRoot, 'jest.config.json')); + expect(config).to.deep.contain(readConfig); + }); + it('should allow users to configure a package.json file as "configFile"', () => { // Arrange fileExistsSyncStub diff --git a/packages/jest-runner/test/unit/jest-test-adapters/jest-greater-than-25-adapter.spec.ts b/packages/jest-runner/test/unit/jest-test-adapters/jest-greater-than-25-adapter.spec.ts index dfc892ceab..78b106bbbf 100644 --- a/packages/jest-runner/test/unit/jest-test-adapters/jest-greater-than-25-adapter.spec.ts +++ b/packages/jest-runner/test/unit/jest-test-adapters/jest-greater-than-25-adapter.spec.ts @@ -11,11 +11,11 @@ describe(JestGreaterThan25TestAdapter.name, () => { let sut: JestGreaterThan25TestAdapter; let runCLIStub: sinon.SinonStub; - const projectRoot = '/path/to/project'; const fileNameUnderTest = '/path/to/file'; - const jestConfig: Config.InitialOptions = { rootDir: projectRoot }; + let jestConfig: Config.InitialOptions; beforeEach(() => { + jestConfig = { rootDir: '/path/to/project' }; runCLIStub = sinon.stub(jestWrapper, 'runCLI'); runCLIStub.resolves({ config: jestConfig, @@ -25,63 +25,69 @@ describe(JestGreaterThan25TestAdapter.name, () => { sut = testInjector.injector.injectClass(JestGreaterThan25TestAdapter); }); - it('should call the runCLI method with the correct ---projectRoot', async () => { - await sut.run({ jestConfig, projectRoot }); - expect(runCLIStub).calledWith(sinon.match.object, [projectRoot]); + it('should call the runCLI method with the correct --projectRoot', async () => { + await sut.run({ jestConfig }); + expect(runCLIStub).calledWith(sinon.match.object, [jestConfig.rootDir!]); + }); + + it('should call the runCLI method with --projectRoot = cwd when no rootDir is provided', async () => { + delete jestConfig.rootDir; + await sut.run({ jestConfig }); + expect(runCLIStub).calledWith(sinon.match.object, [process.cwd()]); }); it('should call the runCLI method with the --findRelatedTests flag when provided', async () => { - await sut.run({ jestConfig, projectRoot, fileNameUnderTest }); + await sut.run({ jestConfig, fileNameUnderTest }); expect(runCLIStub).calledWith( sinon.match({ $0: 'stryker', _: [fileNameUnderTest], - config: JSON.stringify({ rootDir: projectRoot }), + config: JSON.stringify(jestConfig), findRelatedTests: true, runInBand: true, silent: true, testNamePattern: undefined, }), - [projectRoot] + [jestConfig.rootDir!] ); }); it('should call the runCLI method with the --testNamePattern flag when provided', async () => { - await sut.run({ jestConfig, projectRoot, testNamePattern: 'Foo should bar' }); + await sut.run({ jestConfig, testNamePattern: 'Foo should bar' }); expect(runCLIStub).calledWith( sinon.match({ $0: 'stryker', _: [], - config: JSON.stringify({ rootDir: projectRoot }), + config: JSON.stringify(jestConfig), findRelatedTests: false, runInBand: true, silent: true, testNamePattern: 'Foo should bar', }), - [projectRoot] + [jestConfig.rootDir!] ); }); it('should call the runCLI method with the --testLocationInResults flag when provided', async () => { - await sut.run({ jestConfig, projectRoot, testLocationInResults: true }); + await sut.run({ jestConfig, testLocationInResults: true }); expect(runCLIStub).calledWith( sinon.match({ testLocationInResults: true, }), - [projectRoot] + [jestConfig.rootDir!] ); }); it('should call the runCLI method without the --testLocationInResults flag when not', async () => { - await sut.run({ jestConfig, projectRoot, testLocationInResults: false }); - expect(runCLIStub).calledWith(sinon.match({ testLocationInResults: false }), [projectRoot]); + await sut.run({ jestConfig, testLocationInResults: false }); + expect(runCLIStub).calledWith(sinon.match({ testLocationInResults: false }), [jestConfig.rootDir!]); }); it('should call the runCLI method and return the test result', async () => { - const result = await sut.run({ jestConfig, projectRoot }); + const result = await sut.run({ jestConfig }); expect(result).to.deep.equal({ config: jestConfig, @@ -90,7 +96,7 @@ describe(JestGreaterThan25TestAdapter.name, () => { }); it('should call the runCLI method and return the test result when run with --findRelatedTests flag', async () => { - const result = await sut.run({ jestConfig, projectRoot, fileNameUnderTest }); + const result = await sut.run({ jestConfig, fileNameUnderTest }); expect(result).to.deep.equal({ config: jestConfig, diff --git a/packages/jest-runner/test/unit/jest-test-runner.spec.ts b/packages/jest-runner/test/unit/jest-test-runner.spec.ts index 5754efc3ee..691cf6bc64 100644 --- a/packages/jest-runner/test/unit/jest-test-runner.spec.ts +++ b/packages/jest-runner/test/unit/jest-test-runner.spec.ts @@ -112,7 +112,7 @@ describe(JestTestRunner.name, () => { const sut = createSut(); testInjector.logger.isTraceEnabled.returns(true); await sut.dryRun({ coverageAnalysis: 'off' }); - expect(testInjector.logger.trace).calledWithMatch(/Invoking Jest with config\s.*/, sinon.match(/.*"jestConfig".*"projectRoot".*/)); + expect(testInjector.logger.trace).calledWithMatch(/Invoking Jest with config\s.*/, sinon.match(/.*"jestConfig".*/)); }); it('should call the jestTestRunner run method and return a correct runResult', async () => { @@ -481,7 +481,6 @@ describe(JestTestRunner.name, () => { expect(jestTestAdapterMock.run).calledWithExactly( sinon.match({ jestConfig: sinon.match.object, - projectRoot: sinon.match.string, testNamePattern: undefined, fileNameUnderTest: '.stryker-tmp/sandbox2/foo.js', }) @@ -495,7 +494,6 @@ describe(JestTestRunner.name, () => { expect(jestTestAdapterMock.run).calledWithExactly( sinon.match({ jestConfig: sinon.match.object, - projectRoot: sinon.match.string, testNamePattern: undefined, fileNameUnderTest: undefined, }) diff --git a/packages/jest-runner/test/unit/utils/create-react-jest-config.spec.ts b/packages/jest-runner/test/unit/utils/create-react-jest-config.spec.ts deleted file mode 100644 index 201f24a6b6..0000000000 --- a/packages/jest-runner/test/unit/utils/create-react-jest-config.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { assert, expect } from 'chai'; -import sinon from 'sinon'; - -import { createReactJestConfig, createReactTsJestConfig } from '../../../src/utils/create-react-jest-config'; - -describe('create-jest-config', () => { - let requireStub: sinon.SinonStub; - - beforeEach(() => { - requireStub = sinon.stub(); - requireStub.returns(() => 'jestConfig'); - }); - - describe('createReactJestConfig', () => { - it('should call the loader with the react jest config generator', () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - createReactJestConfig(() => {}, '/path/to/project', false, requireStub as unknown as NodeRequire); - - assert(requireStub.calledWith('react-scripts/scripts/utils/createJestConfig')); - }); - - it('should return a jest config', () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - expect(createReactJestConfig(() => {}, '/path/to/project', false, requireStub as unknown as NodeRequire)).to.equal('jestConfig'); - }); - }); - describe('createReactTsJestConfig', () => { - it('should call the loader with the react jest config generator', () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - createReactTsJestConfig(() => {}, '/path/to/project', false, requireStub as unknown as NodeRequire); - - assert(requireStub.calledWith('react-scripts-ts/scripts/utils/createJestConfig')); - }); - - it('should return a jest config', () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - expect(createReactTsJestConfig(() => {}, '/path/to/project', false, requireStub as unknown as NodeRequire)).to.equal('jestConfig'); - }); - }); -}); diff --git a/packages/jest-runner/testResources/config-in-child-dir/client/jest.config.js b/packages/jest-runner/testResources/config-in-child-dir/client/jest.config.js new file mode 100644 index 0000000000..3e253bb91c --- /dev/null +++ b/packages/jest-runner/testResources/config-in-child-dir/client/jest.config.js @@ -0,0 +1,7 @@ +const project = "client" +const prefix = "/../" + +module.exports = { + testEnvironment: "node", + testMatch: [prefix + project + "/**/*.test.js"], +} diff --git a/packages/jest-runner/testResources/config-in-child-dir/client/src/sum.js b/packages/jest-runner/testResources/config-in-child-dir/client/src/sum.js new file mode 100644 index 0000000000..715ee7862c --- /dev/null +++ b/packages/jest-runner/testResources/config-in-child-dir/client/src/sum.js @@ -0,0 +1,8 @@ +function sum(a, b) { + return a + b; +} +function sub(a, b) { + return a - b; +} +exports.sum = sum; +exports.sub = sub; diff --git a/packages/jest-runner/testResources/config-in-child-dir/client/src/sum.test.js b/packages/jest-runner/testResources/config-in-child-dir/client/src/sum.test.js new file mode 100644 index 0000000000..56a145b981 --- /dev/null +++ b/packages/jest-runner/testResources/config-in-child-dir/client/src/sum.test.js @@ -0,0 +1,8 @@ +const {sum, sub} = require('./sum'); + +test('adds 1 + 2 to equal 3', () => { + expect(sum(1, 2)).toBe(3); +}); +test('sub 1 - 0 to equal 1', () => { + expect(sub(1, 0)).toBe(1); +});