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

Commit

Permalink
feat(typescript): Add support for create-react-app using Typescript (#48
Browse files Browse the repository at this point in the history
)

Add support for create-react-app-ts projects

* Add a sample create-react-app-ts project
* Add ConfigLoader for CRA (TypeScript)
* Update README

BREAKING CHANGE: Require Jest 22.x or higher. Support for 20.x and 21.x is dropped.
  • Loading branch information
mthmulders authored and nicojs committed May 11, 2018
1 parent 017139d commit e7c313d
Show file tree
Hide file tree
Showing 35 changed files with 12,738 additions and 170 deletions.
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

0 comments on commit e7c313d

Please sign in to comment.