Skip to content
This repository has been archived by the owner on Feb 19, 2021. It is now read-only.

Add support for create-react-app using Typescript #48

Merged
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
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ For the minimum supported versions, see the peerDependencies section in package.

## Configuration

### Configuring Stryker
Make sure you set the `testRunner` option to "jest" and set `coverageAnalysis` to "off" in your Stryker configuration.

```javascript
Expand All @@ -32,7 +33,8 @@ Make sure you set the `testRunner` option to "jest" and set `coverageAnalysis` t
}
```

The stryker-jest-runner also provides a couple of configurable options using the `jest` property in your stryker config:
### Configuring Jest
The stryker-jest-runner also provides a couple of configurable options using the `jest` property in your Stryker config:

```javascript
{
Expand All @@ -43,14 +45,16 @@ The stryker-jest-runner also provides a couple of configurable options using the
}
```

| option | description | default value |
|----|----|----|
| project (optional) | The type of project you are working on. Currently "react" and "default" are supported. When "react" is configured, "react-scripts" is used (for create-react-app projects). When "default" is configured, your "config" option is used. | default |
| config (optional) | A custom jest configuration (you can also use `require` to load your config here) | undefined |
| option | description | default value | alternative values |
|----|----|----|---|
| project (optional) | The type of project you are working on. | `default` | `default` uses the `config` option (see below)|
| | | | `react` when you are using [create-react-app](https://github.com/facebook/create-react-app) |
| | | | `react-ts` when you are using [create-react-app-typescript](https://github.com/wmonk/create-react-app-typescript) |
| config (optional) | A custom Jest configuration object. You could also use `require` to load it here) | undefined | |

**Note:** When neither of the options are specified it will use the jest configuration in your "package.json". \
**Note:** the `project` option is ignored when the `config` option is specified. \
**Note:** Stryker currently only works for CRA-projects that have not been _ejected_.
**Note:** When neither of the options are specified it will use the Jest configuration in your "package.json". \
**Note:** the `project` option is ignored when the `config` option is specified.
**Note:** Stryker currently only works for CRA-projects that have not been [_ejected_](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-eject).

The following is an example stryker.conf.js file:

Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "tslint -c tslint.json \"src/**/*[^.d$].ts\" \"test/**/*[^.d$].ts\"",
"pretest": "npm run build",
"test": "npm run mocha && npm run stryker",
"mocha": "nyc --reporter=html --report-dir=reports/coverage --check-coverage --lines 85 --functions 90 --branches 65 mocha \"test/integration/**/*.js\" \"test/unit/**/*.js\"",
"mocha": "nyc --reporter=html --report-dir=reports/coverage --check-coverage --lines 85 --functions 90 --branches 65 mocha \"test/unit/**/*.js\" \"test/integration/**/*.js\"",
"stryker": "stryker run",
"preversion": "npm test",
"version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md",
Expand Down Expand Up @@ -47,18 +47,20 @@
"homepage": "https://github.com/stryker-mutator/stryker-jest-runner#readme",
"devDependencies": {
"@types/chai": "^4.0.4",
"@types/log4js": "0.0.32",
"@types/mocha": "^2.2.43",
"@types/node": "^8.5.1",
"@types/semver": "^5.4.0",
"@types/sinon": "^2.3.6",
"chai": "^4.1.2",
"conventional-changelog-cli": "^1.3.9",
"jest": "^20.0.4",
"jest": "^22.0.0",
"mocha": "^5.0.0",
"nyc": "^11.2.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-scripts": "^1.0.17",
"react-scripts-ts": "^2.15.1",
"rimraf": "^2.6.2",
"sinon": "^4.0.1",
"stryker": "^0.21.0",
Expand All @@ -76,6 +78,7 @@
"jest": "^20.0.0"
},
"dependencies": {
"log4js": "^1.1.1",
"semver": "^5.4.1"
}
}
8 changes: 6 additions & 2 deletions src/JestConfigEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Config, ConfigEditor } from 'stryker-api/config';
import JestConfigLoader from './configLoaders/JestConfigLoader';
import DefaultJestConfigLoader from './configLoaders/DefaultJestConfigLoader';
import ReactScriptsJestConfigLoader from './configLoaders/ReactScriptsJestConfigLoader';
import ReactScriptsTSJestConfigLoader from './configLoaders/ReactScriptsTSJestConfigLoader';
import JestConfiguration from './configLoaders/JestConfiguration';
import JEST_OVERRIDE_OPTIONS from './jestOverrideOptions';

Expand All @@ -29,10 +30,13 @@ export default class JestConfigEditor implements ConfigEditor {
switch (project.toLowerCase()) {
case DEFAULT_PROJECT_NAME:
configLoader = new DefaultJestConfigLoader(process.cwd(), fs);
break;
break;
case 'react':
configLoader = new ReactScriptsJestConfigLoader(process.cwd());
break;
break;
case 'react-ts':
configLoader = new ReactScriptsTSJestConfigLoader(process.cwd());
break;
default:
throw new Error(`No configLoader available for ${project}`);
}
Expand Down
3 changes: 3 additions & 0 deletions src/JestTestRunner.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { getLogger } from 'log4js';
import { RunnerOptions, RunResult, TestRunner, RunStatus, TestResult, TestStatus } from 'stryker-api/test_runner';
import { EventEmitter } from 'events';
import JestTestAdapterFactory from './jestTestAdapters/JestTestAdapterFactory';

export default class JestTestRunner extends EventEmitter implements TestRunner {
private log = getLogger(JestTestRunner.name);
private jestConfig: any;
private projectRoot: string;

public constructor(options: RunnerOptions) {
super();

this.projectRoot = process.cwd();
this.log.debug(`Project root is ${this.projectRoot}`);

this.jestConfig = options.strykerOptions.jest.config;
this.jestConfig.rootDir = this.projectRoot;
Expand Down
2 changes: 1 addition & 1 deletion src/configLoaders/ReactScriptsJestConfigLoader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import JestConfigLoader from './JestConfigLoader';
import createReactJestConfig from '../utils/createReactJestConfig';
import { createReactJestConfig } from '../utils/createReactJestConfig';
import * as path from 'path';
import JestConfiguration from './JestConfiguration';

Expand Down
35 changes: 35 additions & 0 deletions src/configLoaders/ReactScriptsTSJestConfigLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import JestConfigLoader from './JestConfigLoader';
import { createReactTsJestConfig } from '../utils/createReactJestConfig';
import * as path from 'path';
import JestConfiguration from './JestConfiguration';

export default class ReactScriptsTSJestConfigLoader implements JestConfigLoader {
private loader: NodeRequire;
private projectRoot: string;

public constructor(projectRoot: string, loader?: NodeRequire) {
this.loader = loader || /* istanbul ignore next */ require;
this.projectRoot = projectRoot;
}

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

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

// Set test environment to jsdom (otherwise Jest won't run)
jestConfiguration.testEnvironment = 'jsdom';

return jestConfiguration;
}

private createJestConfig(reactScriptsTsLocation: string): any {
return createReactTsJestConfig(
(relativePath: string): string => path.join(reactScriptsTsLocation, relativePath),
this.projectRoot,
false
);
}
}
23 changes: 0 additions & 23 deletions src/jestTestAdapters/JestCallbackTestAdapter.ts

This file was deleted.

7 changes: 6 additions & 1 deletion src/jestTestAdapters/JestPromiseTestAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { getLogger } from 'log4js';

import JestTestAdapter from './JestTestAdapter';

export default class JestPromiseTestAdapter implements JestTestAdapter {
private log = getLogger(JestPromiseTestAdapter.name);
private testRunner: any;

public constructor(loader?: NodeRequire) {
Expand All @@ -11,9 +14,11 @@ export default class JestPromiseTestAdapter implements JestTestAdapter {

public run(jestConfig: any, projectRoot: string): Promise<any> {
jestConfig.reporters = [];
const config = JSON.stringify(jestConfig);
this.log.trace(`Invoking Jest with config ${config}`);

return this.testRunner.runCLI({
config: JSON.stringify(jestConfig),
config: config,
runInBand: true,
silent: true
}, [projectRoot]);
Expand Down
12 changes: 7 additions & 5 deletions src/jestTestAdapters/JestTestAdapterFactory.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { getLogger } from 'log4js';

import JestTestAdapter from './JestTestAdapter';
import JestPromiseAdapter from './JestPromiseTestAdapter';
import JestCallbackAdapter from './JestCallbackTestAdapter';
import * as semver from 'semver';

export default class JestTestAdapterFactory {
private static log = getLogger(JestTestAdapterFactory.name);

public static getJestTestAdapter(loader?: NodeRequire): JestTestAdapter {
const jestVersion = this.getJestVersion(loader || /* istanbul ignore next */ require);

if (semver.satisfies(jestVersion, '<20.0.0')) {
throw new Error('You need Jest version >= 20.0.0 to use Stryker');
} else if (semver.satisfies(jestVersion, '>=20.0.0 <21.0.0')) {
return new JestCallbackAdapter();
if (semver.satisfies(jestVersion, '<22.0.0')) {
JestTestAdapterFactory.log.debug(`Detected Jest below 22.0.0`);
throw new Error('You need Jest version >= 22.0.0 to use Stryker');
} else {
return new JestPromiseAdapter();
}
Expand Down
14 changes: 11 additions & 3 deletions src/utils/createReactJestConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export default function createReactJestConfig(resolve: Function, projectRoot: string, ejected: boolean, loader?: NodeRequire): string {
const resolveCreateJestConfig = (path: string, loader?: NodeRequire): Function => {
loader = loader || /* istanbul ignore next */ require;

return loader('react-scripts/scripts/utils/createJestConfig')(resolve, projectRoot, ejected);
}
return loader(path);
};

export function createReactJestConfig(resolve: Function, projectRoot: string, ejected: boolean, loader?: NodeRequire): string {
return resolveCreateJestConfig('react-scripts/scripts/utils/createJestConfig', loader)(resolve, projectRoot, ejected);
}

export function createReactTsJestConfig(resolve: Function, projectRoot: string, ejected: boolean, loader?: NodeRequire): string {
return resolveCreateJestConfig('react-scripts-ts/scripts/utils/createJestConfig', loader)(resolve, projectRoot, ejected);
}
66 changes: 62 additions & 4 deletions test/integration/JestConfigEditorSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expect } from 'chai';
import * as sinon from 'sinon';
import * as path from 'path';

describe('Integration JestConfigEditor', () => {
describe('Integration test for Jest ConfigEditor', () => {
let jestConfigEditor: JestConfigEditor;
let sandbox: sinon.SinonSandbox;
let getProjectRootStub: sinon.SinonStub;
Expand All @@ -25,7 +25,7 @@ describe('Integration JestConfigEditor', () => {

afterEach(() => sandbox.restore());

it('should create a jest configuration for a react project', () => {
it('should create a Jest configuration for a React project', () => {
config.set({ jest: { project: 'react' } });

jestConfigEditor.edit(config);
Expand Down Expand Up @@ -73,7 +73,65 @@ describe('Integration JestConfigEditor', () => {
expect(config.jest.config).to.deep.equal(expectedResult);
});

it('should load the jest configuration from the jest.config.js', () => {
it('should create a Jest configuration for a React + TypeScript project', () => {
config.set({ jest: { project: 'react-ts' } });

jestConfigEditor.edit(config);

const expectedResult = {
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}'
],
globals: {
'ts-jest': {
tsConfigFile: path.join(projectRoot, 'testResources', 'reactTsProject', 'tsconfig.test.json'),
},
},
setupFiles: [path.join(projectRoot, 'node_modules', 'react-scripts-ts', 'config', 'polyfills.js')],
testMatch: [
'<rootDir>/src/**/__tests__/**/*.(j|t)s?(x)',
'<rootDir>/src/**/?(*.)(spec|test).(j|t)s?(x)'
],
testEnvironment: 'jsdom',
testURL: 'http://localhost',
transform: {
'^.+\\.(js|jsx|mjs)$': path.join(projectRoot, 'node_modules', 'react-scripts-ts', 'config', 'jest', 'babelTransform.js'),
'^.+\\\.css$': path.join(projectRoot, 'node_modules', 'react-scripts-ts', 'config', 'jest', 'cssTransform.js'),
'^(?!.*\\.(js|jsx|mjs|css|json)$)': path.join(projectRoot, 'node_modules', 'react-scripts-ts', 'config', 'jest', 'fileTransform.js'),
'^.+\\.tsx?$': path.join(projectRoot, 'node_modules', 'react-scripts-ts', 'config', 'jest', 'typescriptTransform.js'),
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$'
],
moduleNameMapper: {
'^react-native$': 'react-native-web'
},
moduleFileExtensions: [
'web.ts',
'ts',
'web.tsx',
'tsx',
'web.js',
'js',
'web.jsx',
'jsx',
'json',
'node',
'mjs'
],
rootDir: projectRoot,
setupTestFrameworkScriptFile: undefined,
testResultsProcessor: undefined,
collectCoverage: false,
verbose: false,
bail: false
};

// Parse the json back to an object in order to match
expect(config.jest.config).to.deep.equal(expectedResult);
});

it('should load the Jest configuration from the jest.config.js', () => {
getProjectRootStub.returns(path.join(process.cwd(), 'testResources', 'exampleProjectWithExplicitJestConfig'));

jestConfigEditor.edit(config);
Expand All @@ -91,7 +149,7 @@ describe('Integration JestConfigEditor', () => {
});
});

it('should load the jest configuration from the package.json', () => {
it('should load the Jest configuration from the package.json', () => {
getProjectRootStub.returns(path.join(process.cwd(), 'testResources', 'exampleProject'));

jestConfigEditor.edit(config);
Expand Down
Loading