Skip to content

Commit

Permalink
feat(jest-runner): allow configuration in a custom package.json
Browse files Browse the repository at this point in the history
Add support for specifying a different `package.json` file location for your jest configuration.

For example:

```json
{
  "jest": {
      "configFile": "client/package.json"
   }
}
```
  • Loading branch information
nicojs committed Jun 14, 2021
1 parent da40c64 commit 825548c
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 179 deletions.
24 changes: 13 additions & 11 deletions docs/jest-runner.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Install @stryker-mutator/jest-runner locally within your project folder, like so

```bash
npm i --save-dev @stryker-mutator/jest-runner
# OR
yarn add --dev @stryker-mutator/jest-runner
```

## Peer dependencies
Expand Down Expand Up @@ -59,7 +61,7 @@ Configure where jest should get its configuration from.

Default: `undefined`

The path to your Jest config file.
The path to your Jest config file of package.json file containing in the `"jest"` key. By default, the @stryker-mutator/jest-runner will try to look for "jest.conf.js" or "package.json" in the current working directory.

### `jest.config` [`object`]

Expand All @@ -80,15 +82,7 @@ The `@stryker-mutator/jest-runner` plugin supports coverage analysis and test fi

### Coverage reporting

When using `"all"` or `"perTest"` coverage analysis, this plugin reports mutant coverage by hooking into the [jest's test environment](https://jestjs.io/docs/en/configuration.html#testenvironment-string). The test environment setting is overridden based on the `"testEnvironment"` configuration option in your jest config:

Jest test environment|Jest runner's override|
---|---
node|@stryker-mutator/jest-runner/jest-env/node
jsdom|@stryker-mutator/jest-runner/jest-env/jsom
jest-environment-jsdom-sixteen|@stryker-mutator/jest-runner/jest-env/jsom-sixteen

As long as you're using one of these test environments, you won't have to do anything.
When using `"all"` or `"perTest"` coverage analysis, this plugin reports mutant coverage by hooking into the [jest's test environment](https://jestjs.io/docs/en/configuration.html#testenvironment-string). The test environment setting in your configuration file is overridden by default and you won't have to do anything here.

However, if you choose to override the jest-environment on a file-by-file basis using [jest's `@jest-environment` docblock](https://jestjs.io/docs/en/configuration.html#testenvironment-string), you will have to do the work.

Expand All @@ -108,6 +102,14 @@ Becomes:
*/
```

This is the list of jest environments that are shipped with @stryker-mutator/jest-runner.

Jest test environment|@stryker-mutator/jest-runner override|
---|---
node|@stryker-mutator/jest-runner/jest-env/node
jsdom|@stryker-mutator/jest-runner/jest-env/jsom
jest-environment-jsdom-sixteen|@stryker-mutator/jest-runner/jest-env/jsom-sixteen

Don't worry; using Stryker's alternative is harmless during regular unit testing.

If you're using a custom test environment, you'll need to mixin the Stryker functionality yourself:
Expand All @@ -126,6 +128,6 @@ module.exports = mixinJestEnvironment(MyCustomTestEnvironment);

### Test filtering

When using `"perTest"` coverage analysis, the `@stryker-mutator/jest-runner` will hook into the [jest test runner](https://jestjs.io/docs/en/configuration.html#testrunner-string). Both the default `"jasmine2"` as well as [jest-circus](https://www.npmjs.com/package/jest-circus) are supported here.
When using `"perTest"` coverage analysis, the `@stryker-mutator/jest-runner` will hook into the [jest test runner](https://jestjs.io/docs/en/configuration.html#testrunner-string). Both `"jasmine2"` as well as [`jest-circus`](https://www.npmjs.com/package/jest-circus) (default) are supported here.

If you're using a different test runner, you're out of luck. Please downgrade to using `"all"` coverage analysis. If you think we should support your test runner, please let us know by opening an [issue](https://github.com/stryker-mutator/stryker-js/issues/new?assignees=&labels=%F0%9F%9A%80+Feature+request&template=feature_request.md&title=), or by joining our [slack channel](https://join.slack.com/t/stryker-mutator/shared_invite/enQtOTUyMTYyNTg1NDQ0LTU4ODNmZDlmN2I3MmEyMTVhYjZlYmJkOThlNTY3NTM1M2QxYmM5YTM3ODQxYmJjY2YyYzllM2RkMmM1NjNjZjM).
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,66 @@ import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
import { StrykerOptions } from '@stryker-mutator/api/core';
import { Config } from '@jest/types';

import { loader, projectRoot } from '../plugin-tokens';
import { requireResolve } from '@stryker-mutator/util';

import { JestRunnerOptionsWithStrykerOptions } from '../jest-runner-options-with-stryker-options';

import { JestConfigLoader } from './jest-config-loader';
import { NodeRequireFunction } from './node-require-function';

/**
* The Default config loader will load the Jest configuration using the package.json in the package root
*/
export class CustomJestConfigLoader implements JestConfigLoader {
public static inject = tokens(commonTokens.logger, commonTokens.options, loader, projectRoot);
public static inject = tokens(commonTokens.logger, commonTokens.options);

constructor(
private readonly log: Logger,
private readonly options: StrykerOptions,
private readonly require: NodeRequireFunction,
private readonly root: string
) {}
constructor(private readonly log: Logger, private readonly options: StrykerOptions) {}

public loadConfig(): Config.InitialOptions {
const jestConfig = this.readConfigFromJestConfigFile() || this.readConfigFromPackageJson() || {};
const jestConfig = this.readConfigFromJestConfigFile() ?? this.readConfigFromPackageJson() ?? {};
this.log.debug('Final jest config: %s', jestConfig);
return jestConfig;
}

private readConfigFromJestConfigFile() {
try {
const jestOptions = this.options as JestRunnerOptionsWithStrykerOptions;
const configFilePath = path.join(this.root, jestOptions.jest?.configFile ?? 'jest.config.js');
const config = this.require(configFilePath);
private readConfigFromJestConfigFile(): Config.InitialOptions | undefined {
const configFilePath = this.resolveJestConfigFilePath();
if (configFilePath) {
const config = requireResolve(configFilePath) as Config.InitialOptions;
this.log.debug(`Read Jest config from ${configFilePath}`);
this.setRootDir(config, configFilePath);
return config;
} catch {
/* Don't return anything (implicitly return undefined) */
}
return undefined;
}

private readConfigFromPackageJson() {
try {
const configFilePath = path.join(this.root, 'package.json');
const config = JSON.parse(fs.readFileSync(configFilePath, 'utf8')).jest;
this.log.debug(`Read Jest config from ${configFilePath}`);
private readConfigFromPackageJson(): Config.InitialOptions | undefined {
const pkgJsonFilePath = this.resolvePackageJsonFilePath();
if (pkgJsonFilePath) {
const config: Config.InitialOptions = JSON.parse(fs.readFileSync(pkgJsonFilePath, 'utf8')).jest ?? {};
this.log.debug(`Read Jest config from ${pkgJsonFilePath}`);
this.setRootDir(config, pkgJsonFilePath);
return config;
} catch {
/* Don't return anything (implicitly return undefined) */
}
return undefined;
}

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))) {
return packageJsonCandidate;
}
return undefined;
}
private setRootDir(config: Config.InitialOptions, configFilePath: string) {
config.rootDir = path.resolve(path.dirname(configFilePath), config.rootDir ?? '.');
}

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))) {
return configFileCandidate;
}
return undefined;
}
}
8 changes: 1 addition & 7 deletions packages/jest-runner/src/config-loaders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { tokens, commonTokens, Injector, PluginContext } from '@stryker-mutator/
import { StrykerOptions } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';

import { requireResolve } from '@stryker-mutator/util';

import { JestRunnerOptionsWithStrykerOptions } from '../jest-runner-options-with-stryker-options';
import * as pluginTokens from '../plugin-tokens';

Expand All @@ -22,11 +20,7 @@ export function configLoaderFactory(
}
};
const optionsWithJest: JestRunnerOptionsWithStrykerOptions = options as JestRunnerOptionsWithStrykerOptions;

const configLoaderInjector = injector
.provideValue(pluginTokens.loader, requireResolve)
.provideValue(pluginTokens.resolve, require.resolve)
.provideValue(pluginTokens.projectRoot, process.cwd());
const configLoaderInjector = injector.provideValue(pluginTokens.resolve, require.resolve);

switch (optionsWithJest.jest.projectType) {
case 'custom':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,32 @@ import path from 'path';
import { tokens } from '@stryker-mutator/api/plugin';
import { Config } from '@jest/types';

import { createReactJestConfig } from '../utils';
import { PropertyPathBuilder, requireResolve } from '@stryker-mutator/util';

import * as pluginTokens from '../plugin-tokens';
import { JestRunnerOptionsWithStrykerOptions } from '../jest-runner-options-with-stryker-options';

import { JestConfigLoader } from './jest-config-loader';

export class ReactScriptsJestConfigLoader implements JestConfigLoader {
public static inject = tokens(pluginTokens.resolve, pluginTokens.projectRoot);
public static inject = tokens(pluginTokens.resolve);

constructor(private readonly resolve: RequireResolve, private readonly projectRoot: string) {}
constructor(private readonly resolve: RequireResolve) {}

public loadConfig(): Config.InitialOptions {
try {
// Get the location of react script, this is later used to generate the Jest configuration used for React projects.
const reactScriptsLocation = path.join(this.resolve('react-scripts/package.json'), '..');

// Create the React configuration for Jest
const jestConfiguration = this.createJestConfig(reactScriptsLocation);
const jestConfiguration = this.createJestConfig();

return jestConfiguration;
} catch (e) {
if (this.isNodeErrnoException(e) && e.code === 'MODULE_NOT_FOUND') {
throw Error('Unable to locate package react-scripts. This package is required when projectType is set to "create-react-app".');
throw Error(
`Unable to locate package "react-scripts". This package is required when "${PropertyPathBuilder.create<JestRunnerOptionsWithStrykerOptions>()
.prop('jest')
.prop('projectType')
.build()}" is set to "create-react-app".`
);
}
throw e;
}
Expand All @@ -34,7 +38,13 @@ export class ReactScriptsJestConfigLoader implements JestConfigLoader {
return arg.code !== undefined;
}

private createJestConfig(reactScriptsLocation: string): Config.InitialOptions {
return createReactJestConfig((relativePath: string): string => path.join(reactScriptsLocation, relativePath), this.projectRoot, false);
private createJestConfig(): Config.InitialOptions {
const createReactJestConfig = requireResolve('react-scripts/scripts/utils/createJestConfig') as (
resolve: (thing: string) => string,
rootDir: string,
isEjecting: boolean
) => Config.InitialOptions;
const reactScriptsLocation = path.join(this.resolve('react-scripts/package.json'), '..');
return createReactJestConfig((relativePath) => path.join(reactScriptsLocation, relativePath), process.cwd(), false);
}
}
25 changes: 0 additions & 25 deletions packages/jest-runner/src/utils/create-react-jest-config.ts

This file was deleted.

6 changes: 6 additions & 0 deletions packages/jest-runner/test/helpers/producers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { TestResult, AggregatedResult, AssertionResult, SerializableError } from '@jest/test-result';
import type { EnvironmentContext } from '@jest/environment';
import { Circus, Config } from '@jest/types';
import { factory } from '@stryker-mutator/test-helpers';

import { JestOptions } from '../../src-generated/jest-runner-options';
import { JestRunResult } from '../../src/jest-run-result';
import { JestRunnerOptionsWithStrykerOptions } from '../../src/jest-runner-options-with-stryker-options';

export const createJestRunnerOptionsWithStrykerOptions = (overrides?: Partial<JestOptions>): JestRunnerOptionsWithStrykerOptions => {
return factory.strykerWithPluginOptions({ jest: createJestOptions(overrides) });
};

export const createJestOptions = (overrides?: Partial<JestOptions>): JestOptions => {
return {
Expand Down
Loading

0 comments on commit 825548c

Please sign in to comment.