From 630b60b074cd757aaaf7ad46ed1dcbb7c76f07a1 Mon Sep 17 00:00:00 2001 From: Sander Koenders Date: Tue, 6 Feb 2018 07:38:32 +0100 Subject: [PATCH] feat(Jest-runner): support jest configuration (#25) Support configurable options using the `jest` property in your stryker config: ``` js jest: { project: 'react', // or 'default' config: require('path/to/your/custom/jestConfig.js') } ``` When neither of the options are specified, the jest config in your `package.json` file is used. To support reading jest configuration, we are now using a higher level jest api, the `jest-cli`, to start the tests. In order to keep it in a single process, the `run-in-band` option is provided, forcing jest to run inside the parent process. --- .bettercodehub.yml | 4 + .bithoundrc | 2 +- .gitignore | 28 ++- .npmignore | 18 +- .travis.yml | 22 +- .vscode/launch.json | 143 ++----------- .vscode/settings.json | 1 + CHANGELOG.md | 1 - CONTRIBUTING.md | 49 ----- Gruntfile.js | 126 ------------ LICENSE | 2 +- README.md | 65 +++--- package.json | 97 ++++----- src/JestConfigEditor.ts | 37 ++++ src/JestTestRunner.ts | 140 +++---------- src/JestVersionAdapters.ts | 86 -------- src/configLoaders/DefaultJestConfigLoader.ts | 29 +++ src/configLoaders/JestConfigLoader.ts | 18 ++ src/configLoaders/JestConfiguration.ts | 60 ++++++ .../ReactScriptsJestConfigLoader.ts | 35 ++++ src/index.ts | 8 +- .../JestCallbackTestAdapter.ts | 23 +++ .../JestPromiseTestAdapter.ts | 21 ++ src/jestTestAdapters/JestTestAdapter.ts | 3 + .../JestTestAdapterFactory.ts | 24 +++ src/utils/createReactJestConfig.ts | 5 + stryker-80x80.png | Bin 6194 -> 0 bytes stryker.conf.js | 26 +++ test/helpers/registerChaiPlugins.ts | 5 - test/helpers/testResultProducer.ts | 122 +++++++++++ test/integration/JestConfigEditorSpec.ts | 94 +++++++++ test/integration/JestTestRunner.it.ts | 194 ------------------ test/integration/StrykerJestRunnerSpec.ts | 86 ++++++++ test/unit/JestConfigEditorSpec.ts | 56 +++++ test/unit/JestTestRunnerSpec.ts | 117 +++++++++++ .../configLoaders/DefaultConfigLoaderSpec.ts | 43 ++++ .../configLoaders/ReactConfigLoaderSpec.ts | 54 +++++ test/unit/index.spec.ts | 42 ---- .../JestCallbackTestAdapterSpec.ts | 62 ++++++ .../JestPromiseTestAdapterSpec.ts | 70 +++++++ .../JestTestAdapterFactorySpec.ts | 64 ++++++ test/unit/utils/createReactJestConfig.ts | 32 +++ testResources/exampleProject/package.json | 14 ++ .../src/Add.js | 2 +- .../src/Circle.js | 2 +- .../src/__tests__/AddSpec.js | 9 +- .../src/__tests__/CircleSpec.js | 4 +- testResources/reactProject/.gitignore | 21 ++ testResources/reactProject/package.json | 16 ++ testResources/reactProject/public/favicon.ico | Bin 0 -> 3870 bytes testResources/reactProject/public/index.html | 40 ++++ .../reactProject/public/manifest.json | 15 ++ testResources/reactProject/src/App.css | 28 +++ testResources/reactProject/src/App.js | 21 ++ testResources/reactProject/src/App.test.js | 8 + testResources/reactProject/src/index.css | 5 + testResources/reactProject/src/index.js | 8 + testResources/reactProject/src/logo.svg | 7 + .../reactProject/src/registerServiceWorker.js | 108 ++++++++++ .../__tests__/isAdult.spec.js | 7 - .../roots-option-project/lib/isAdult.js | 3 - testResources/sampleProject/src/Error.js | 2 - .../sampleProject/src/InfiniteAdd.js | 7 - .../src/__tests__/AddFailedSpec.js | 22 -- .../sampleProject/src/__tests__/EmptySpec.js | 0 .../sampleProject/src/__tests__/ErrorSpec.js | 7 - .../src/__tests__/FailingAddSpec.js | 13 -- testResources/sampleProject/stryker.conf.js | 8 - tsconfig.json | 13 +- tslint.json | 112 +++++----- 70 files changed, 1619 insertions(+), 997 deletions(-) create mode 100644 .bettercodehub.yml delete mode 100644 CONTRIBUTING.md delete mode 100644 Gruntfile.js create mode 100644 src/JestConfigEditor.ts delete mode 100644 src/JestVersionAdapters.ts create mode 100644 src/configLoaders/DefaultJestConfigLoader.ts create mode 100644 src/configLoaders/JestConfigLoader.ts create mode 100644 src/configLoaders/JestConfiguration.ts create mode 100644 src/configLoaders/ReactScriptsJestConfigLoader.ts create mode 100644 src/jestTestAdapters/JestCallbackTestAdapter.ts create mode 100644 src/jestTestAdapters/JestPromiseTestAdapter.ts create mode 100644 src/jestTestAdapters/JestTestAdapter.ts create mode 100644 src/jestTestAdapters/JestTestAdapterFactory.ts create mode 100644 src/utils/createReactJestConfig.ts delete mode 100644 stryker-80x80.png create mode 100644 stryker.conf.js delete mode 100644 test/helpers/registerChaiPlugins.ts create mode 100644 test/helpers/testResultProducer.ts create mode 100644 test/integration/JestConfigEditorSpec.ts delete mode 100644 test/integration/JestTestRunner.it.ts create mode 100644 test/integration/StrykerJestRunnerSpec.ts create mode 100644 test/unit/JestConfigEditorSpec.ts create mode 100644 test/unit/JestTestRunnerSpec.ts create mode 100644 test/unit/configLoaders/DefaultConfigLoaderSpec.ts create mode 100644 test/unit/configLoaders/ReactConfigLoaderSpec.ts delete mode 100644 test/unit/index.spec.ts create mode 100644 test/unit/jestTestAdapters/JestCallbackTestAdapterSpec.ts create mode 100644 test/unit/jestTestAdapters/JestPromiseTestAdapterSpec.ts create mode 100644 test/unit/jestTestAdapters/JestTestAdapterFactorySpec.ts create mode 100644 test/unit/utils/createReactJestConfig.ts create mode 100644 testResources/exampleProject/package.json rename testResources/{sampleProject => exampleProject}/src/Add.js (99%) rename testResources/{sampleProject => exampleProject}/src/Circle.js (98%) rename testResources/{sampleProject => exampleProject}/src/__tests__/AddSpec.js (79%) rename testResources/{sampleProject => exampleProject}/src/__tests__/CircleSpec.js (81%) create mode 100644 testResources/reactProject/.gitignore create mode 100644 testResources/reactProject/package.json create mode 100644 testResources/reactProject/public/favicon.ico create mode 100644 testResources/reactProject/public/index.html create mode 100644 testResources/reactProject/public/manifest.json create mode 100644 testResources/reactProject/src/App.css create mode 100644 testResources/reactProject/src/App.js create mode 100644 testResources/reactProject/src/App.test.js create mode 100644 testResources/reactProject/src/index.css create mode 100644 testResources/reactProject/src/index.js create mode 100644 testResources/reactProject/src/logo.svg create mode 100644 testResources/reactProject/src/registerServiceWorker.js delete mode 100644 testResources/roots-option-project/__tests__/isAdult.spec.js delete mode 100644 testResources/roots-option-project/lib/isAdult.js delete mode 100644 testResources/sampleProject/src/Error.js delete mode 100644 testResources/sampleProject/src/InfiniteAdd.js delete mode 100644 testResources/sampleProject/src/__tests__/AddFailedSpec.js delete mode 100644 testResources/sampleProject/src/__tests__/EmptySpec.js delete mode 100644 testResources/sampleProject/src/__tests__/ErrorSpec.js delete mode 100644 testResources/sampleProject/src/__tests__/FailingAddSpec.js delete mode 100644 testResources/sampleProject/stryker.conf.js diff --git a/.bettercodehub.yml b/.bettercodehub.yml new file mode 100644 index 0000000..68c2a40 --- /dev/null +++ b/.bettercodehub.yml @@ -0,0 +1,4 @@ +component_depth: 1 +languages: +- javascript +- typescript \ No newline at end of file diff --git a/.bithoundrc b/.bithoundrc index 1af1f84..046fc02 100644 --- a/.bithoundrc +++ b/.bithoundrc @@ -7,4 +7,4 @@ "test/**", "testResources/**" ] -} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 477b12f..2c9cf4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,12 @@ -node_modules -npm-debug.log -debug.log -coverage -.tscache -/resources -typings -*.js.map -src/**/*.js -test/**/*.js -src/**/*.d.ts -test/**/*.d.ts -!Gruntfile.js -!*.conf.js -!testResources -reports +.idea +/node_modules +**/*.js +**/*.js.map +**/*.d.ts + +!testResources/**/*.js +!stryker.conf.js +/package-lock.json +/reports +/.nyc_output +.stryker-tmp \ No newline at end of file diff --git a/.npmignore b/.npmignore index 8519d34..78c082e 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,10 @@ -*.ts +**/* !*.d.ts -Gruntfile.js -test -tsconfig.json -typings.json -testResources -typings -.vscode -*.png +!bin/** +!src/** +src/**/*.map +src/**/*.ts +!src/**/*.d.ts +!README.md +!LICENSE +!CHANGELOG.md \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index cc56a64..7bc4555 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,13 @@ language: node_js node_js: - - "7" - - "6" -env: - - JEST_VERSION=18.0.0 - - JEST_VERSION=19.0.0 - - JEST_VERSION=20.0.0 - - JEST_VERSION=21.0.0 +- node +- 'lts/*' +- '6' +- '4' install: npm install -before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - # Install desired version of Jest - - npm i -D jest@${JEST_VERSION} jest-cli@${JEST_VERSION} \ No newline at end of file +before_install: +- if [[ `npm -v` = 2* ]]; then npm i -g npm@3; fi +notifications: + email: + on_success: never + on_failure: change \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 5b88261..2471c2f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,123 +1,24 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Run unit tests", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/node_modules/grunt/bin/grunt", - // "preLaunchTask": "build", - "stopOnEntry": false, - "args": [ - "mochaTest:unit" - ], - "cwd": "${workspaceRoot}/.", - "runtimeExecutable": null, - "runtimeArgs": [ - "--nolazy" - ], - "env": { - "NODE_ENV": "development" - }, - "externalConsole": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}" - }, - { - "name": "Run integration tests", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/node_modules/grunt-cli/bin/grunt", - // "preLa4unchTask": "ts", - "stopOnEntry": false, - "args": [ - "mochaTest:integration" - ], - "cwd": "${workspaceRoot}/.", - "runtimeExecutable": null, - "runtimeArgs": [ - "--nolazy" - ], - "env": { - "NODE_ENV": "development" - }, - "externalConsole": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}" - }, - { - "name": "Run stryker example", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/bin/stryker", - "preLaunchTask": "build", - "stopOnEntry": false, - "args": [ - "--configFile", - "testResources/sampleProject/stryker.conf.js", - "--logLevel", - "trace", - "--testFramework", - "jasmine" - ], - "cwd": "${workspaceRoot}", - "runtimeExecutable": null, - "runtimeArgs": [ - "--nolazy" - ], - "env": { - "NODE_ENV": "development" - }, - "externalConsole": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}" - }, - { - "name": "Run own dog food", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/bin/stryker", - "preLaunchTask": "build", - "stopOnEntry": false, - "args": [ - "--configFile", - "stryker.conf.js", - "--logLevel", - "info" - ], - "cwd": "${workspaceRoot}", - "runtimeExecutable": null, - "runtimeArgs": [ - "--nolazy" - ], - "env": { - "NODE_ENV": "development" - }, - "externalConsole": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}" - }, - { - "name": "Run stryker help", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/src/Stryker.js", - "preLaunchTask": "build", - "stopOnEntry": false, - "args": [ - "--help" - ], - "cwd": "${workspaceRoot}/.", - "runtimeExecutable": null, - "runtimeArgs": [ - "--nolazy" - ], - "env": { - "NODE_ENV": "development" - }, - "externalConsole": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}" - } - ] + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Integration tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "-u", + "tdd", + "--timeout", + "999999", + "--colors", + "${workspaceFolder}/test/helpers/**/*.js", + "${workspaceFolder}/test/integration/**/*.js" + ], + "internalConsoleOptions": "openOnSessionStart" + } + ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f4cdf1b..fcc94df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "files": { "exclude": { ".git": "", + ".tscache": "", "**/*.js": { "when": "$(basename).ts" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2171e..015a432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,4 +102,3 @@ * fix(README): Fix Travis Badge pointing to the wrong repo ([9177447](https://github.com/stryker-mutator/stryker-jest-runner/commit/9177447)) - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 2bd8a8a..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,49 +0,0 @@ -# Contribute to Stryker - -This is the contribution guide for Stryker. Great to have you here! Here are a few ways you can help make this project better. - -## Learn & listen - -Get in touch with us through twitter or via the [Stryker gitter](https://gitter.im/stryker-mutator/stryker) - -## Adding new features - -New features are welcome! Either as requests or proposals. - -1. Please create an issue first or let us know via the [Stryker gitter](https://gitter.im/stryker-mutator/stryker) -2. Create a fork on your github account. -3. When writing your code, please conform to the existing coding style. - See [.editorconfig](https://github.com/stryker-mutator/stryker/blob/master/.editorconfig) and the [typescript guidelines](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines) -4. Please create or edit unit tests or integration tests. -5. Run the tests using `npm test` -6. When creating commits, please conform to [the angular commit message style](https://docs.google.com/document/d/1rk04jEuGfk9kYzfqCuOlPTSJw3hEDZJTBN5E5f1SALo/edit). - Namely in the form `(): \n\n[body]` - * Type: feat, fix, docs, style, refactor, test, chore. - * Scope can the the file or group of files (not a strict right or wrong) - * Subject and body: present tense (~changed~*change*, ~added~*add*) and include motivation and contrasts with previous behavior - - -Don't get discouraged! We estimate that the response time from the -maintainers is around a day or so. - -# Bug triage - -Found a bug? Don't worry, we'll fix it, or you can ;) - -Please report a bug report on our [issues page](https://github.com/stryker-mutator/stryker/issues). In this please: - -1. Label the issue as bug -2. Explain what the bug is in clear English -3. Include reproduction steps - This can be an example project, code snippit, etc -4. Include the expected behaviour. -5. Include actual behaviour. -6. Add more details if required (i.e. which browser, which test runner, which versions, etc) - -# Community -Do you want to help? Great! These are a few things you can do: - -* Evangelize mutation testing - Mutation testing is still relatively new, especially in JavaScript. Please help us get the word out there! - Share your stories in blog posts an on social media. Please inform us about it! -* Did you use Stryker? Your feedback is very valuable to us. Good and bad! Please contact us and let us know what you think diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 5b42561..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict'; - - -module.exports = function (grunt) { - - require('load-grunt-tasks')(grunt); - - grunt.initConfig({ - - clean: { - build: { - src: ['+(test|src)/**/*+(.d.ts|.js|.map)', '*+(.js|.d.ts|.map)', '!src/resources/**/*.*', '!Gruntfile.js', '!protractor.conf.js'] - }, - coverage: { - src: ['coverage'] - }, - reports: { - src: ['reports'] - } - }, - - watch: { - testFiles: { - files: ['test/**/*.js'], - tasks: ['mochaTest:unit'] - } - }, - mochaTest: { - unit: { - options: { - reporter: 'spec' - }, - src: ['test/helpers/**/*.js', 'test/unit/**/*.js'] - }, - integration: { - options: { - reporter: 'spec', - timeout: 5000 - }, - // Register helpers before, it includes a log4js mock which has to be loaded as early as possible - src: ['test/helpers/**/*.js', 'test/integration/**/*.js'] - } - }, - mocha_istanbul: { - coverage: { - // Register helpers before, it includes a log4js mock which has to be loaded as early as possible - src: ['test/helpers/**/*.js', 'test/unit/**/*.js', 'test/integration/**/*.js'], - } - }, - istanbul_check_coverage: { - default: { - options: { - coverageFolder: 'coverage*', - check: { - lines: 80, - statements: 80 - } - } - } - }, - - ts: { - options: { - failOnTypeErrors: true - }, - build: { - tsconfig: { - passThrough: true - } - }, - }, - 'npm-contributors': { - options: { - commitMessage: 'chore: update contributors' - } - }, - conventionalChangelog: { - release: { - options: { - changelogOpts: { - preset: 'angular' - } - }, - src: 'CHANGELOG.md' - } - }, - bump: { - options: { - commitFiles: [ - 'package.json', - 'CHANGELOG.md' - ], - commitMessage: 'chore: release v%VERSION%', - prereleaseName: 'rc' - } - }, - tslint: { - src: { - src: ['*.ts', 'src/**/*.ts'] - }, - test: { - src: ['test/**/*.ts', 'testResources/module/*.ts'] - } - } - }); - - - grunt.registerTask('release', 'Build, bump and publish to NPM.', function (type) { - grunt.task.run([ - 'test', - 'npm-contributors', - 'bump:' + (type || 'patch') + ':bump-only', - 'conventionalChangelog', - 'bump-commit', - 'npm-publish' - ]); - }); - - grunt.registerTask('default', ['test']); - grunt.registerTask('watch-test', ['test', 'watch']); - grunt.registerTask('test', ['build', 'coverage']); - grunt.registerTask('build', ['clean', 'tslint', 'ts']); - grunt.registerTask('integration', ['mochaTest:integration']); - grunt.registerTask('coverage', ['mocha_istanbul:coverage']); - grunt.registerTask('serve', ['watch']); -}; diff --git a/LICENSE b/LICENSE index 8dada3e..9c8f3ea 100644 --- a/LICENSE +++ b/LICENSE @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 99c9f1d..7245651 100644 --- a/README.md +++ b/README.md @@ -3,33 +3,56 @@ ![Stryker](https://github.com/stryker-mutator/stryker/raw/master/stryker-80x80.png) -# Stryker Jest Runner -A plugin to use the [Jest](http://facebook.github.io/jest/) test runner in [Stryker](http://stryker-mutator.github.io), the JavaScript mutation testing framework. +# Stryker-jest-runner ## Installation - Install stryker-jest-runner locally within your project folder, like so: ```bash npm i --save-dev stryker-jest-runner ``` -### Peer dependencies +## Peer dependencies +The stryker-jest-runner is a plugin for Stryker to enable Jest as a test runner. As such, you should make sure you have the correct versions of its dependencies installed: -The `stryker-jest-runner` is a plugin for Stryker to enable Jest as a test runner. -As such, you should make sure you have the correct versions of its dependencies installed: +- jest +- stryker-api -* `jest-cli` -* `jest-runtime` +For the minimum supported versions, see the peerDependencies section in package.json. -For the minimum supported versions, see the `peerDependencies` section in [package.json](https://github.com/stryker-mutator/stryker-jest-runner/blob/master/package.json). -For all supported version, see the `env` section in [.travis.yml](https://github.com/stryker-mutator/stryker-jest-runner/blob/master/.travis.yml). +## Configuration -## Configuring +Make sure you set the `testRunner` option to "jest" and set `coverageAnalysis` to "off" in your Stryker configuration. -The following is an example stryker.conf.js file that will include the tests in your `__tests__` directories and snapshots in your `__snapshots__` directories. +```javascript +{ + testRunner: 'jest' + coverageAnalysis: 'off' +} +``` + +The stryker-jest-runner also provides a couple of configurable options using the `jest` property in your stryker config: +```javascript +{ + jest: { + project: 'default', + config: require('path/to/your/custom/jestConfig.js') + } +} ``` + +| option | description | default value | +|----|----|----| +| project (optional) | The 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 | + +**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. + +The following is an example stryker.conf.js file that will include the tests in your `__tests__` directories and snapshots in your `__snapshots__` directories. + +```javascript module.exports = function(config) { config.set({ files: [ @@ -50,20 +73,8 @@ module.exports = function(config) { For more information on what these options mean, take a look at the [Stryker readme](https://github.com/stryker-mutator/stryker/tree/master/packages/stryker#readme). -For the time being, the Jest runner uses a default configuration for [Jest CLI options](https://facebook.github.io/jest/docs/en/cli.html). But pull requests are - obviously - welcome. - -### Loading the plugin - -In order to use the `stryker-jest-runner` it must be loaded in the Stryker mutation testing framework via the Stryker configuration. -The easiest way to achieve this, is *not have a `plugins` section* in your config file. That way, all `node_modules` starting with `stryker-` will be loaded. - -### Using the test runner - -In order to use Jest as the test runner, you simply specify it in your config file: `testRunner: 'jest'`. -Note that coverageAnalysis is not yet supported, so you must explicitly set it to `off` in your Stryker configration. -Again, pull requests are - obviously - welcome. +## Loading the plugin +In order to use the `stryker-jest-runner` it must be loaded in the Stryker mutation testing framework via the Stryker configuration. The easiest way to achieve this, is not have a plugins section in your config file. That way, all node_modules starting with `stryker-` will be loaded. ## Contributing - -Before you start hacking along, make sure to install a supported version of Jest inside your working copy. -The versions supported are listed above. \ No newline at end of file +Make sure to read the Stryker contribution guidelines located in the [Stryker mono repository](https://github.com/stryker-mutator/stryker/blob/master/CONTRIBUTING.md). diff --git a/package.json b/package.json index f573b3e..0a607b0 100644 --- a/package.json +++ b/package.json @@ -3,76 +3,69 @@ "version": "0.3.0", "description": "A plugin to use the jest test runner and framework in Stryker, the JavaScript mutation testing framework", "main": "src/index.js", - "engines": { - "node": ">=6.0.0" - }, "scripts": { - "test": "grunt test" + "build": "tsc", + "premocha": "npm run build", + "lint": "tslint -c tslint.json \"src/**/*[^.d$].ts\" \"test/**/*[^.d$].ts\"", + "test": "npm run lint && 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\"", + "stryker": "stryker run" }, "repository": { "type": "git", "url": "https://github.com/stryker-mutator/stryker-jest-runner.git" }, + "engines": { + "node": ">=4" + }, "keywords": [ "stryker", "stryker-plugin", "jest", "stryker-test-runner" ], - "author": "nicojs", + "author": "Sander koenders ", + "contributors": [ + "Maarten Mulders ", + "mshogren ", + "Nico Jansen ", + "Simon de Lang ", + "Philipp Weissenbacher " + ], "license": "Apache-2.0", "bugs": { "url": "https://github.com/stryker-mutator/stryker-jest-runner/issues" }, - "homepage": "https://github.com/stryker-mutator/stryker-jest-runner", - "peerDependencies": { - "jest-cli": ">=18.0.0", - "jest-runtime": ">=18.0.0", - "stryker-api": "^0.10.0" - }, + "homepage": "https://github.com/stryker-mutator/stryker-jest-runner#readme", "devDependencies": { - "@types/chai": "^3.4.32", - "@types/chai-as-promised": "0.0.29", - "@types/estree": "0.0.34", - "@types/lodash": "^4.14.37", - "@types/log4js": "0.0.32", - "@types/mocha": "^2.2.32", - "@types/node": "^6.0.45", + "@types/chai": "^4.0.4", + "@types/mocha": "^2.2.43", + "@types/node": "^8.5.1", "@types/semver": "^5.4.0", - "@types/sinon": "^1.16.29", - "@types/sinon-chai": "^2.7.26", - "chai": "^3.5.0", - "chai-as-promised": "^6.0.0", - "grunt": "^1.0.1", - "grunt-bump": "^0.8.0", - "grunt-cli": "^1.2.0", - "grunt-contrib-clean": "^1.0.0", - "grunt-contrib-watch": "^1.0.0", - "grunt-conventional-changelog": "^6.1.0", - "grunt-mocha-istanbul": "^5.0.1", - "grunt-mocha-test": "^0.13.2", - "grunt-npm": "0.0.2", - "grunt-ts": "^6.0.0-beta.3", - "grunt-tslint": "^4.0.0", - "istanbul": "^0.4.4", - "load-grunt-tasks": "^3.5.2", - "mocha": "^3.0.2", - "sinon": "^1.17.5", - "sinon-chai": "^2.8.0", - "stryker-api": "^0.9.0", - "tslint": "^4.0.2", - "typescript": "^2.0.3" + "@types/sinon": "^2.3.6", + "chai": "^4.1.2", + "jest": "^20.0.4", + "mocha": "^5.0.0", + "nyc": "^11.2.1", + "react": "^16.2.0", + "react-dom": "^16.2.0", + "react-scripts": "^1.0.17", + "sinon": "^4.0.1", + "stryker": "^0.15.5", + "stryker-api": "^0.11.0", + "stryker-html-reporter": "^0.11.3", + "stryker-mocha-framework": "^0.7.1", + "stryker-mocha-runner": "^0.10.1", + "stryker-typescript": "^0.8.0", + "tslib": "^1.8.1", + "tslint": "^5.8.0", + "typescript": "^2.5.0" }, - "dependencies": { - "lodash": "^4.13.1", - "log4js": "^1.0.1", - "semver": "5.4.1" + "peerDependencies": { + "stryker-api": "^0.11.0", + "jest": "^20.0.0" }, - "contributors": [ - "Maarten Mulders ", - "mshogren ", - "Nico Jansen ", - "Simon de Lang ", - "Philipp Weissenbacher " - ] + "dependencies": { + "semver": "^5.4.1" + } } diff --git a/src/JestConfigEditor.ts b/src/JestConfigEditor.ts new file mode 100644 index 0000000..3c2f34f --- /dev/null +++ b/src/JestConfigEditor.ts @@ -0,0 +1,37 @@ +import * as fs from 'fs'; +import { Config, ConfigEditor } from 'stryker-api/config'; +import JestConfigLoader from './configLoaders/JestConfigLoader'; +import DefaultJestConfigLoader from './configLoaders/DefaultJestConfigLoader'; +import ReactScriptsJestConfigLoader from './configLoaders/ReactScriptsJestConfigLoader'; + +const DEFAULT_PROJECT_NAME = 'default'; + +export default class JestConfigEditor implements ConfigEditor { + public edit(strykerConfig: Config): void { + // If there is no Jest property on the Stryker config create it + strykerConfig.jest = strykerConfig.jest || {}; + + // When no project is set set it to 'default' + strykerConfig.jest.project = strykerConfig.jest.project || DEFAULT_PROJECT_NAME; + + // When no config property is set load the configuration with the project type + strykerConfig.jest.config = strykerConfig.jest.config || this.getConfigLoader(strykerConfig.jest.project).loadConfig(); + } + + private getConfigLoader(project: string): JestConfigLoader { + let configLoader: JestConfigLoader; + + switch (project.toLowerCase()) { + case DEFAULT_PROJECT_NAME: + configLoader = new DefaultJestConfigLoader(process.cwd(), fs); + break; + case 'react': + configLoader = new ReactScriptsJestConfigLoader(process.cwd()); + break; + default: + throw new Error(`No configLoader available for ${project}`); + } + + return configLoader; + } +} \ No newline at end of file diff --git a/src/JestTestRunner.ts b/src/JestTestRunner.ts index 6bf0731..674c895 100644 --- a/src/JestTestRunner.ts +++ b/src/JestTestRunner.ts @@ -1,131 +1,49 @@ -import { RunnerOptions, RunResult, RunStatus, TestResult, TestRunner, TestStatus } from 'stryker-api/test_runner'; +import { RunnerOptions, RunResult, TestRunner, RunStatus, TestResult, TestStatus } from 'stryker-api/test_runner'; import { EventEmitter } from 'events'; - -import * as _ from 'lodash'; -const os = require('os'); -const path = require('path'); - -import * as log4js from 'log4js'; -const log = log4js.getLogger('JestTestRunner'); - -import { findJestAdapter, JestAdapter } from './JestVersionAdapters'; - -const DEFAULT_OPTIONS: Object = { - cacheDirectory: path.join(os.tmpdir(), 'jest'), - collectCoverage: false, - haste: {}, - maxWorkers: 1, - moduleFileExtensions: ['js', 'json', 'jsx', 'node'], - moduleNameMapper: [], - setupFiles: [], - snapshotSerializers: [], - testEnvironment: 'jest-environment-jsdom', - testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.jsx?$', - testRunner: 'jest-jasmine2', - verbose: true -}; +import JestTestAdapterFactory from './jestTestAdapters/JestTestAdapterFactory'; export default class JestTestRunner extends EventEmitter implements TestRunner { - // Seems there are no up-to-date TypeScript definitions for Jest - - private jestAdapter: JestAdapter; - private hasteContext: Promise; - private options: any; - private paths: string[]; + private jestConfig: any; + private projectRoot: string; - constructor(options: RunnerOptions) { + public constructor(options: RunnerOptions) { super(); - this.jestAdapter = findJestAdapter(); - log.debug(`Received options ${JSON.stringify(options)}`); - this.options = _.assign(DEFAULT_OPTIONS, { - rootDir: process.cwd(), - roots: [process.cwd()], - testPathDirs: [process.cwd()] - }); - log.debug(`Using options ${JSON.stringify(this.options)}`); - - const _testRegex = new RegExp(this.options.testRegex); - const isTest = (file: string) => _testRegex.test(file); + this.projectRoot = process.cwd(); - this.paths = options.files - .map(file => file.name) - .filter(isTest); - - log.info(`Discovered specs ${JSON.stringify(this.paths)}`); + this.jestConfig = options.strykerOptions.jest.config; + this.jestConfig.rootDir = this.projectRoot; } - init(): Promise | void { - log.info(`Initializing Jest`); - this.hasteContext = this.jestAdapter.buildHasteContext(this.options); - } + public async run(): Promise { + const jestTestRunner = JestTestAdapterFactory.getJestTestAdapter(); - run(): Promise { - return this.hasteContext - .then((context: any) => this.runTests(context)) - .then((results: any[]) => this.processJestResults(results)) - .catch((error: Error) => this.catchError(error)); - } + const { results } = await jestTestRunner.run(this.jestConfig, process.cwd()); - private runTests(hasteContext: any): Promise { - const promises = this.paths.map((specPath: string) => { - return this.jestAdapter.runTest( - path.resolve(specPath), - this.options, - hasteContext.resolver - ); - }); - return Promise.all(promises); - } + // Map the failureMessages from each testSuite to a single array then filter out empty errors + const errorMessages = results.testResults.map((testSuite: any) => testSuite.failureMessage).filter((errorMessage: string) => errorMessage); - private catchError(error: Error): RunResult { - log.error(`An error occurred while invoking Jest: ${error.stack}`); return { - coverage: undefined, - tests: [], - status: RunStatus.Error, - errorMessages: [ `${error.name}: ${error.message}` ] + tests: this.processTestResults(results.testResults), + status: (results.numRuntimeErrorTestSuites > 0) ? RunStatus.Error : RunStatus.Complete, + errorMessages }; } - private processJestResults(results: any): RunResult { - log.debug(`Jest returned ${JSON.stringify(results.length)} results`); - const aggregatedResults = results - .map((result: any) => result.testResults) - .reduce((result1: any[], result2: any[]) => result1.concat(result2), []); - - const tests = this.createTestResults(aggregatedResults); - const status = RunStatus.Complete; - const coverage: any = undefined; - const errorMessages = this.createErrorMessages(aggregatedResults); + private processTestResults(suiteResults: Array): Array { + const testResults: Array = []; - return { tests, status, coverage, errorMessages }; - } - - private createTestResults(results: any[]): TestResult[] { - return results.map((result: any) => { - const name = result.fullName; - const status = this.createTestStatus(result); - const timeSpentMs = result.duration; - const failureMessages = result.failureMessages; - - return { name, status, timeSpentMs, failureMessages }; - }); - } - - private createTestStatus(result: any): TestStatus { - switch (result.status) { - case 'passed': return TestStatus.Success; - case 'failed': return TestStatus.Failed; - default : log.warn(`Got unexpected test status ${result.status}`); return null; + for (let suiteResult of suiteResults) { + for (let testResult of suiteResult.testResults) { + testResults.push({ + name: testResult.fullName, + status: (testResult.status === 'passed') ? TestStatus.Success : TestStatus.Failed, + timeSpentMs: testResult.duration, + failureMessages: testResult.failureMessages + }); + } } - } - - private createErrorMessages(result: any[]): string[] { - return result.map((r: any) => r.failureMessage).filter(msg => !!msg); - } - dispose?(): Promise | void { - log.info('Disposing'); + return testResults; } -} +} \ No newline at end of file diff --git a/src/JestVersionAdapters.ts b/src/JestVersionAdapters.ts deleted file mode 100644 index 962a672..0000000 --- a/src/JestVersionAdapters.ts +++ /dev/null @@ -1,86 +0,0 @@ -const pkg = require('jest-cli/package.json'); -const Runtime = require('jest-runtime'); - -const Console = require('console').Console; - -import * as log4js from 'log4js'; -import * as semver from 'semver'; -const log = log4js.getLogger('JestAdapter'); - -const console = new Console(process.stdout, process.stderr); - -/** - * Interface to decouple the runner from various versions of Jest. - * Since the runner hooks into some implementation modules of Jest, - * we cannot expect the API's to be stable at this time. - */ -abstract class JestAdapter { - abstract buildHasteContext(options: any): Promise; - abstract runTest(path: string, options: any, resolver: any): Promise; -} - -class JestPre20Adapter extends JestAdapter { - private _runTest = require('jest-cli/build/runTest'); - - public buildHasteContext(options: any): Promise { - const { maxWorkers } = options; - return Runtime.createHasteContext(options, { console, maxWorkers }); - } - - public runTest(path: string, options: any, resolver: any): Promise { - return this._runTest(path, options, resolver); - } -} - -class Jest20Adapter extends JestAdapter { - private _runTest = require('jest-cli/build/runTest'); - - public buildHasteContext(options: any): Promise { - const { maxWorkers } = options; - return Runtime.createContext(options, { console, maxWorkers }); - } - - public runTest(path: string, options: any, resolver: any): Promise { - const globalConfig = {}; - return this._runTest(path, globalConfig, options, resolver); - } -} - -class Jest21UpAdapter extends JestAdapter { - private _runTest = require('jest-runner/build/run_test').default; - - public buildHasteContext(options: any): Promise { - const { maxWorkers } = options; - return Runtime.createContext(options, { console, maxWorkers }); - } - - public runTest(path: string, options: any, resolver: any): Promise { - const globalConfig = {}; - return this._runTest(path, globalConfig, options, resolver); - } -} - -const findJestAdapter: () => JestAdapter = () => { - const jestVersion = pkg.version; - log.debug(`Found Jest CLI v${jestVersion}`); - - if (!jestVersion) { - throw new Error(`Did not find package.json for jest-cli. Is it installed?`); - } - - if (semver.satisfies(jestVersion, '<20.0.0')) { - log.info('Detected Jest before v20.0.0'); - return new JestPre20Adapter(); - } else if (semver.satisfies(jestVersion, '>=20.0.0 <21.0.0')) { - log.info('Detected Jest v20'); - return new Jest20Adapter(); - } else { - log.info('Detected Jest v21'); - return new Jest21UpAdapter(); - } -}; - -export { - JestAdapter, - findJestAdapter, -} \ No newline at end of file diff --git a/src/configLoaders/DefaultJestConfigLoader.ts b/src/configLoaders/DefaultJestConfigLoader.ts new file mode 100644 index 0000000..1aa8ae8 --- /dev/null +++ b/src/configLoaders/DefaultJestConfigLoader.ts @@ -0,0 +1,29 @@ +import JestConfigLoader from './JestConfigLoader'; +import * as path from 'path'; +import JestConfiguration from './JestConfiguration'; + +/** + * The Default config loader will load the Jest configuration using the package.json in the package root + */ +export default class DefaultJestConfigLoader implements JestConfigLoader { + private _fs: any; + private _projectRoot: string; + + constructor(projectRoot: string, fs: any) { + this._projectRoot = projectRoot; + this._fs = fs; + } + + public loadConfig(): JestConfiguration { + const packageJson = this._fs.readFileSync(path.join(this._projectRoot, 'package.json'), 'utf8'); + + // Parse the package.json and return the Jest property + const jestConfig = JSON.parse(packageJson).jest; + + if (!jestConfig) { + throw new Error('No Jest configuration found in your package.json'); + } + + return jestConfig; + } +} \ No newline at end of file diff --git a/src/configLoaders/JestConfigLoader.ts b/src/configLoaders/JestConfigLoader.ts new file mode 100644 index 0000000..00b77f9 --- /dev/null +++ b/src/configLoaders/JestConfigLoader.ts @@ -0,0 +1,18 @@ +import JestConfiguration from './JestConfiguration'; + +/** + * The Configloader interface is used to load different kinds of configurations for Jest. + * Custom ConfigLoaders should implement this interface, the ConfigEditor will then be able to use it to load a Jest configuration. + * + * ConfigLoaders are typically used for projects that do not provide their configuration via the package.json file (e.g. React). + * The loaderConfig method will return a usable config for Jest to use. + */ + +export default interface JestConfigLoader { + /* + * Load the JSON representation of a Jest Configuration. + * + * @return {JestConfiguration} an object containing the Jest configuration. + */ + loadConfig(): JestConfiguration; +} \ No newline at end of file diff --git a/src/configLoaders/JestConfiguration.ts b/src/configLoaders/JestConfiguration.ts new file mode 100644 index 0000000..0fb99f5 --- /dev/null +++ b/src/configLoaders/JestConfiguration.ts @@ -0,0 +1,60 @@ +// Grabbed from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/jest/index.d.ts + +export type Path = string; +export type Glob = string; +export type ConfigGlobals = object; + +// flow's Maybe type https://flow.org/en/docs/types/primitives/#toc-maybe-types +export type Maybe = void | null | undefined | T; // tslint:disable-line:void-return + +export interface HasteConfig { + defaultPlatform?: Maybe; + hasteImplModulePath?: string; + platforms?: string[]; + providesModuleNodeModules: string[]; +} + +export default interface JestConfiguration { + automock: boolean; + browser: boolean; + cache: boolean; + cacheDirectory: Path; + clearMocks: boolean; + coveragePathIgnorePatterns: string[]; + cwd: Path; + detectLeaks: boolean; + displayName: Maybe; + forceCoverageMatch: Glob[]; + globals: ConfigGlobals; + haste: HasteConfig; + moduleDirectories: string[]; + moduleFileExtensions: string[]; + moduleLoader: Path; + moduleNameMapper: Array<[string, string]>; + modulePathIgnorePatterns: string[]; + modulePaths: string[]; + name: string; + resetMocks: boolean; + resetModules: boolean; + resolver: Maybe; + rootDir: Path; + roots: Path[]; + runner: string; + setupFiles: Path[]; + setupTestFrameworkScriptFile: Path; + skipNodeResolution: boolean; + snapshotSerializers: Path[]; + testEnvironment: string; + testEnvironmentOptions: object; + testLocationInResults: boolean; + testMatch: Glob[]; + testPathIgnorePatterns: string[]; + testRegex: string; + testRunner: string; + testURL: string; + timers: 'real' | 'fake'; + transform: Array<[string, Path]>; + transformIgnorePatterns: Glob[]; + unmockedModulePathPatterns: Maybe; + watchPathIgnorePatterns: string[]; +} diff --git a/src/configLoaders/ReactScriptsJestConfigLoader.ts b/src/configLoaders/ReactScriptsJestConfigLoader.ts new file mode 100644 index 0000000..1918e6d --- /dev/null +++ b/src/configLoaders/ReactScriptsJestConfigLoader.ts @@ -0,0 +1,35 @@ +import JestConfigLoader from './JestConfigLoader'; +import createReactJestConfig from '../utils/createReactJestConfig'; +import * as path from 'path'; +import JestConfiguration from './JestConfiguration'; + +export default class ReactScriptsJestConfigLoader 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 script, this is later used to generate the Jest configuration used for React projects. + const reactScriptsLocation = path.join(this.loader.resolve('react-scripts/package.json'), '..'); + + // Create the React configuration for Jest + const jestConfiguration = this.createJestConfig(reactScriptsLocation); + + // Set test environment to jsdom (otherwise Jest won't run) + jestConfiguration.testEnvironment = 'jsdom'; + + return jestConfiguration; + } + + private createJestConfig(reactScriptsLocation: string): any { + return createReactJestConfig( + (relativePath: string): string => path.join(reactScriptsLocation, relativePath), + this.projectRoot, + false + ); + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b39a295..174e1a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ +import { ConfigEditorFactory } from 'stryker-api/config'; import { TestRunnerFactory } from 'stryker-api/test_runner'; +import JestConfigEditor from './JestConfigEditor'; import JestTestRunner from './JestTestRunner'; -// This is the main file loaded when stryker loads this plugin -// Report your plugin to the correct Factory +process.env.BABEL_ENV = 'test'; -TestRunnerFactory.instance().register('jest', JestTestRunner); +ConfigEditorFactory.instance().register('jest', JestConfigEditor); +TestRunnerFactory.instance().register('jest', JestTestRunner); \ No newline at end of file diff --git a/src/jestTestAdapters/JestCallbackTestAdapter.ts b/src/jestTestAdapters/JestCallbackTestAdapter.ts new file mode 100644 index 0000000..0f15097 --- /dev/null +++ b/src/jestTestAdapters/JestCallbackTestAdapter.ts @@ -0,0 +1,23 @@ +import JestTestAdapter from './JestTestAdapter'; + +export default class JestCallbackTestAdapter implements JestTestAdapter { + private testRunner: any; + + public constructor(loader?: NodeRequire) { + loader = loader || /* istanbul ignore next */ require; + + this.testRunner = loader('jest'); + } + + public run(jestConfig: any, projectRoot: string): Promise { + jestConfig.reporters = []; + + return new Promise((resolve) => { + this.testRunner.runCLI({ + config: JSON.stringify(jestConfig), + runInBand: true, + silent: true + }, [projectRoot], (results: any) => resolve({ results })); + }); + } +} \ No newline at end of file diff --git a/src/jestTestAdapters/JestPromiseTestAdapter.ts b/src/jestTestAdapters/JestPromiseTestAdapter.ts new file mode 100644 index 0000000..acdc1a6 --- /dev/null +++ b/src/jestTestAdapters/JestPromiseTestAdapter.ts @@ -0,0 +1,21 @@ +import JestTestAdapter from './JestTestAdapter'; + +export default class JestPromiseTestAdapter implements JestTestAdapter { + private testRunner: any; + + public constructor(loader?: NodeRequire) { + loader = loader || /* istanbul ignore next */ require; + + this.testRunner = loader('jest'); + } + + public run(jestConfig: any, projectRoot: string): Promise { + jestConfig.reporters = []; + + return this.testRunner.runCLI({ + config: JSON.stringify(jestConfig), + runInBand: true, + silent: true + }, [projectRoot]); + } +} \ No newline at end of file diff --git a/src/jestTestAdapters/JestTestAdapter.ts b/src/jestTestAdapters/JestTestAdapter.ts new file mode 100644 index 0000000..1c45953 --- /dev/null +++ b/src/jestTestAdapters/JestTestAdapter.ts @@ -0,0 +1,3 @@ +export default interface JestTestAdapter { + run(config: Object, projectRoot: string): Promise; +} \ No newline at end of file diff --git a/src/jestTestAdapters/JestTestAdapterFactory.ts b/src/jestTestAdapters/JestTestAdapterFactory.ts new file mode 100644 index 0000000..b46f0a7 --- /dev/null +++ b/src/jestTestAdapters/JestTestAdapterFactory.ts @@ -0,0 +1,24 @@ +import JestTestAdapter from './JestTestAdapter'; +import JestPromiseAdapter from './JestPromiseTestAdapter'; +import JestCallbackAdapter from './JestCallbackTestAdapter'; +import * as semver from 'semver'; + +export default class JestTestAdapterFactory { + 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(); + } else { + return new JestPromiseAdapter(); + } + } + + private static getJestVersion(loader: NodeRequire): string { + const packageJson = loader('jest/package.json'); + + return packageJson.version; + } +} \ No newline at end of file diff --git a/src/utils/createReactJestConfig.ts b/src/utils/createReactJestConfig.ts new file mode 100644 index 0000000..dcc93ed --- /dev/null +++ b/src/utils/createReactJestConfig.ts @@ -0,0 +1,5 @@ +export default function createReactJestConfig(resolve: Function, projectRoot: string, ejected: boolean, loader?: NodeRequire): string { + loader = loader || /* istanbul ignore next */ require; + + return loader('react-scripts/scripts/utils/createJestConfig')(resolve, projectRoot, ejected); +} \ No newline at end of file diff --git a/stryker-80x80.png b/stryker-80x80.png deleted file mode 100644 index b866f3c73f3520cac61f597f13a8fce8dd6fec69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6194 zcmV-27|rL2P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Rb1P2c=5B6e11ONaR5lKWrRCwCWoqMokM|s$P zU-vm@X71d_-o2}RYFCkjR#>dwLI?{dmSh~e0!~$gotT(XFcz4KaUj`HaVk{BsgxZT z_(3Vbc7cLI!Xpk;9;Ap92UH3m1ZWotmVHP_``)|H`@Zu!r~Atvea_t3t2_73%)M*# zRh^pMJu|1ff9LD3zy7+vUf~+aw2`{er{xt!)Zh$L*GkL``pbESLcpvbl z{9YkNS1x`d9ak@+c{#;1BH-@)`WL{rfjL|;eQP{mKzxP01_R+j9!}9ra(SZ9Ug@PLQ6MowY=lMUkfN$O@FRVGMfvCN=Jn1x+5+^ai9G{c1~1ABz5)0g!6oVJ zs{M-qqWKss;05);oiQSToB*cdll;5=LG7&Pv%ounlO1j93t~?IyTE${_yWOyE8b%N z9nC0sfz+UxfK?>%NZCQ+A@YcTy8JsW0}YF&LBS>V5apha_FQ~nnwFS(s8ZzEQsu_~_4$TzE!uNF{!e1`qPgij# ztLUXNsvu>*gr_$t?4SqU1N?`6A+8Fr3%q{>{1HnoL4UTcFlBX%R5PMGrit#F!tAI8 zV47>2mnDrVJUukGu8P<|Nf#sB$zihZZowTJ1*;JI@cZ7R@Fgu_r2cq65WAMr1>OgN zKkRsO{dHL(*tS%N?wlt1=}FACM!S2q8!g(scI_!%e$y(FI8+69c#PJc@4{axBbBTl zh=o&bfe#TBq$h|gme2*>uM)cLVDO9vNU^8%_A^9xUqLeiEfawCZ#|d93kXOh!_URE zzH|fGu`y(5V2JMl{(UbH*Xn*Yf56WXd?-HTfsd^R8b;B2M0Cd#c4rN(cQ@$jAu22NY0HtXjB@(m5RDmw_w5;QTY+q>jO|L< z_Y1S^zOP2BX1WI&MA4QZe()mMp>b%~&;$B6zaJxn$I_fp`w13aqkT$nmj3~RcHKfU zA$j;bW?KWYm9@1|QDZ6f-aY%rfl*_=kkGhKxZyGy$9-6HR0|1t%+OpNF{YIe)m!)QJ+M zVTaGwj;^Q&F(4LR__Iy47Az+3I|XPFNQOMgu%kH>ar|)1i?vytS8RU)oI(q?=WN=n zCg1j!whb)4v;}y48PEX$D(VCoYay{;)&7ZrsS`=AExo_50{+a|m=oU`;?|#?r?qH^ zD`B48n6*qE8sg0NDqK8LrgkO5$ByRE9QTcnwjH}3ODtGC4m?4K;0T}vz{25>H(9hzb`P zDbG*ObKm$#KM)J^wj*cs@q$^@anle64tjfL52f}p+gQhhL^X`c^XFrp`{ZWk&c#fh zN^qGFC7Sz;iWXg)5aqtmXblRHoY50GGwBSy{}gan-lTQ;eeDMJ;B$#UdBm-UBLK&r z9cKD;LR3;p!yXgs(#)mf;maWwjqRcQ;%j&1tN=`BVBpdG)2;oi9s$;(wFd-+O?%SS z9pWA&8S+HQlD4t>glABQ1)IN0IYWN*mY-}xBl}B-1y)kE)`mfrhR_{Yqnm9MNJ+o z07k&R4F~82DS5KPWB4nju+Y;3#L9TaPrmmu5ADCmo;TJ&p}Ao2S+I}SN-O(-STNcL zM0Wq)J$rzE1|%B}(JHzagFtlK?3#g15%5lMsW7rVW6%9Hw(n~YS3HdwOMN=RXJDcn zyA8kuZ_Zdj6XR{^b@mCaUnY0Lumy_Jh+zNolxE4xc=T6yY2`eXU`V_!USXEpF zV!`UW_JgMQ`K-qjTbM%SH%K8>Aolp_67EN%L^sc4##_NALT`F69a@FjS)XQiP_Rbo2EVO>s9e3-;ShjA@U;PSx!hD%;z~ zs-Lls-?ZzIh0NW*kU3q*oLR^m3z?(keZDjF8cLy!iAS3j^U5fPpDpw1j|`;}3~$Xy zhCJR2>T-r_xjzQ7reSnPMrp)z>iKFIod*OcK+7w2=V&-j-FaHx>$yhL9G$Oy-G2Di z8{TL{e%gqr5iv#tBeJpGwil2RR@=5GWzM`}dGVVGmycA)TEg(wjLMitHE7scRSOV> zEjKrrIg>DbDp?;y3EPZ2*k~kTfUns7d-uE^cpDoSi5^sZZufSNCDb+J>?B-xaVRXb z&sj#cWejb0xKzT9)YU+0ShXA3mT~I2p^dJp5>i;cVE6CcvjzAS`pXi!CemK=SJ+W0 zQ6ppCq-f1D`NJ}&zgxvSxbAgLj3Mk?=u%@5@To9zUB=9rgsBrHqSD}6xI$G$;NKcT z!d|_z{(sdFJz)gAXVVBflQOCCR7N~$T{!spasJ;E*U`%7!o5VHZ+KG;F&hT27)$CR za@-KoM$dNK{41BMAdU~St6ZXyD%L_}%yI683g7(17F;G_Rj1o?1hST4;>IS`35UyM zBl=ju=&V;gWq<}DpX|p5;EE6{C7Lb-Y0G1d^FJ(el9AQ>-mNo1*9Nm%P``g;K7Qk=A(y06O z{BzyA5&kkDHl1Q4sLxogyqXXteob3g-JEUI2w5=tIxGP^K(K>FeXkvj0XCJdsEHbK zLFUiLYj#UjNQV7}EZFANIjeA>@cS&VKuC2ezO=ED@1iG@e0P5TEm>(`X?mqO-$SR# z_aKU_yJ#(88kMHgbS?`%Qm z;t;LwbF-c&DD*w!BP>+s^Q7$VZfp^$2~);_{lS9CD_-?46tE(`2s=guXZmx%N7e-} zPggZJED*h627BuqW}*>pwJbUpBh?Ih>ulKg9cTS)x$S_cL?Ue80&WenDgw<}i(j90 z_k=$6`ADR$5Y?Q^qK=yo@*+M&IA5Uqc7krLa)Jz}lz#CfX7?fzc{DX)gC%+KHu!SU zy;sN%PV~ELzT-s+Y8BmCL%g7?*f#~{&PVI9O@)#zs_Lg*cq_hQZClHV9I;mkS_u8_ z(o)(x^Q9cJ{dF-4w$A9%4pjkK7Q3rP^v27BWi1(nmcj0Mwt9#knGEYJU9`B^fH`+AX7+51O?sOL)v~f8z+SBoj6MGJWlByCo$V=ZC`Pv3k7@gJmp_~nfQTAtHV<;G1R6a zPJXLOR2p3S6wajT|4rR_QdK`}Epfimwpyy!yMWKo?}qanjEtm|-}h3;lys~3e1&PO zIJ9Z;XNSTyn?OA;b|}M4G%>qsm>sq7+E;@|h+LOP9WQ=y6F>NGW0Z#dAS-kZuuk&B zg|DlVkf4op-SrB23PeSjI+^gb-`m=+KT${qL|*ygXl#G6<<(qjw){vG@$TmjFEzDW z9rDlLWa&td-rt`Wq~b`QpCEmHVky~X4d?(g4Z7H+3m2p98t`naq;=uDPi+oqVEu-)OOrWqMP!LUWljc@@BABeDf90Pkigy{RuNBC)6S<=wbrW5{r}Hbhu` z5Lqi|;KSB85We-vElizA23cG$Ruz2a_1&?N^F!9yNJP&!TKxLKqgQ=-wQ;F9SLJDf zCKmZ5y;s8kUTA#&MzX_W$Vj?$u+9dHcS2lNyn}CiVuCXVDyhAShDifSR|@O z)&bPqYn%TXfXhN49+G@?UT<(jLG!**9(Bx| ziFx{?TRHo^Axa}Y-0M$LAabf78?xpJL82FD7c#Ffm1g|r3opIq2fZG9QjE-hLO2UB zC2aHWP4dpK4NLU;S>i_~vAY+8L)~2DPmgfqE2Cr$8FWuf z3dBzJQ$I0Mef(^rWzW{?zMf`UO=3Q<0=!iLE`wN{XyFKl8osx9TWdGDLyU$4cM?yu zZ-(gJE3j*kaz#-uxHK$P;+G6jQMwSOWhT-vB?A_(M4UNL;gx4XmQq~t7~8|S3d`OJ zd9I98ee&V06Tfy~dd^&EG!+TP=KTkcb^UR#u1V*aD(?g42!|T>cY0AiI^%`(zy$sW zo4K-ihN<0CY~R~rWNSug#9{Nq>Z^X5g^*PJLd5jxgbPPXTsT~2@k&ILg!HaQFwPQ71ElaF=wgITGCp0Us1`o_W=$t%W!!uB=h=@$KvP@Gxc^l z&3q)Hs&KmA;9nm(u|9xZAQq=uJOcbDmJ+o4KQ~YyvBJyqbq-B0V%6Xspn_K=4_ty# zOx}fyIc=vs>_ghYWUpLodH=4GHUB3PiKbhr3wwm%Y`wWAyq*De0a+Z2v<>(aVSi0g zxxS9VD{?emUZ`_;ez85zj3gYuSaS|hx!NxFFG|O$l}`1~YtFw%RnHB_QFN)%%8UqQ zYj|O1o=+V=*V9A$(v}rm91C0|9Bup_!odth7_2w~bWJI?kliv|;kJ<~txSnAoLFe^ zzbB`N)ZhoxwgOyHmq=l@<^B8Kee<55zh$^GS&giH__|HmKfP-&k-%?!?^w_9in{kL zE1aX+d0@qF=P--q?jjhmW?6+M9-FUmy4K|BOVj+{i&Om0zMH8#-*08F(l~|8sXkS6 z?jsgCUyZD7daq?;Jiy6XgOB|1)xKE_6d_xtiNz`9mT;E9`w2d5hd;0+A0 zfVd1~=h>u3fL|t@SpEcXiKSC@J7K=&T>Svivv;JzEyF{3ra)JOy6jnnmtYmHWUBwJ z;oO~7Ykz5bxpc^>+UZs%0&YjSM8kRBfAAPTTBr|-Vf}0MU9_Zt==*3|V{Fq4rX< zW$xTG;@*4PO@1cL_^;iOV$U;+!F4rj&g#X>)7T_jems6eClEfoVo|r5>bz+ilK6G#M zYnP_yPuH4LiIKBj^|*(ZTi(C?{)5LZ>tjF9?;d!G(KupTDX|M#CJpc19h*k { + let jestConfigEditor: JestConfigEditor; + let sandbox: sinon.SinonSandbox; + let getProjectRootStub: sinon.SinonStub; + + let projectRoot: string = process.cwd(); + let config: Config; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + getProjectRootStub = sandbox.stub(process, 'cwd'); + getProjectRootStub.returns(projectRoot); + + jestConfigEditor = new JestConfigEditor(); + + config = new Config; + }); + + afterEach(() => sandbox.restore()); + + it('should create a jest configuration for a react project', () => { + config.set({ jest: { project: 'react' } }); + + jestConfigEditor.edit(config); + + const expectedResult = { + collectCoverageFrom: [ + 'src/**/*.{js,jsx,mjs}' + ], + setupFiles: [path.join(projectRoot, 'node_modules', 'react-scripts', 'config', 'polyfills.js')], + testMatch: [ + '/src/**/__tests__/**/*.{js,jsx,mjs}', + '/src/**/?(*.)(spec|test).{js,jsx,mjs}' + ], + testEnvironment: 'jsdom', + testURL: 'http://localhost', + transform: { + '^.+\\.(js|jsx|mjs)$': path.join(projectRoot, 'node_modules', 'react-scripts', 'config', 'jest', 'babelTransform.js'), + '^.+\\\.css$': path.join(projectRoot, 'node_modules', 'react-scripts', 'config', 'jest', 'cssTransform.js'), + '^(?!.*\\.(js|jsx|mjs|css|json)$)': path.join(projectRoot, 'node_modules', 'react-scripts', 'config', 'jest', 'fileTransform.js') + }, + transformIgnorePatterns: [ + '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$' + ], + moduleNameMapper: { + '^react-native$': 'react-native-web' + }, + moduleFileExtensions: [ + 'web.js', + 'mjs', + 'js', + 'json', + 'web.jsx', + 'jsx', + 'node' + ], + rootDir: projectRoot, + setupTestFrameworkScriptFile: undefined + }; + + // 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 package.json', () => { + getProjectRootStub.returns(path.join(process.cwd(), 'testResources', 'exampleProject')); + + jestConfigEditor.edit(config); + + expect(config.jest.project).to.equal('default'); + expect(config.jest.config).to.deep.equal({ + collectCoverage: false, + moduleFileExtensions: ['js', 'json', 'jsx', 'node'], + testEnvironment: 'jest-environment-jsdom', + testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.jsx?$', + testRunner: 'jest-jasmine2', + verbose: true + }); + }); + + it('should return with an error when an invalid project is specified', () => { + const project = 'invalidProject'; + config.set({ jest: { project } }); + + expect(() => jestConfigEditor.edit(config)).to.throw(Error, `No configLoader available for ${project}`); + }); +}); \ No newline at end of file diff --git a/test/integration/JestTestRunner.it.ts b/test/integration/JestTestRunner.it.ts deleted file mode 100644 index fb1c8e5..0000000 --- a/test/integration/JestTestRunner.it.ts +++ /dev/null @@ -1,194 +0,0 @@ -import * as chai from 'chai'; -import JestTestRunner from '../../src/JestTestRunner'; -import { RunnerOptions, RunResult, RunStatus, TestStatus } from 'stryker-api/test_runner'; -import { FileKind, FileDescriptor } from 'stryker-api/core'; -import * as chaiAsPromised from 'chai-as-promised'; -chai.use(chaiAsPromised); -let expect = chai.expect; - -describe('JestTestRunner', function () { - let sut: JestTestRunner; - this.timeout(10000); - - const expectToHaveSuccessfulTests = (result: RunResult, n: number) => { - expect(result.tests.filter(t => t.status === TestStatus.Success)).to.have.length(n); - }; - - const expectToHaveFailedTests = (result: RunResult, expectedFailureMessages: string[]) => { - const actualFailedTests = result.tests.filter(t => t.status === TestStatus.Failed); - expect(actualFailedTests).to.have.length(expectedFailureMessages.length); - actualFailedTests.forEach(failedTest => expect(failedTest.failureMessages[0]).to.contain(expectedFailureMessages.shift())); - }; - - function file(overrides?: Partial): FileDescriptor { - return Object.assign({}, { - name: 'file.js', - transpiled: true, - included: true, - mutated: true, - kind: FileKind.Text - }, overrides); - } - - describe('when all tests succeed', () => { - let testRunnerOptions: RunnerOptions; - - before(() => { - testRunnerOptions = { - files: [ - file({ name: 'testResources/sampleProject/src/Add.js', mutated: true }), - file({ name: 'testResources/sampleProject/src/__tests__/AddSpec.js', mutated: false })], - port: 9877, - strykerOptions: { logLevel: 'trace' } - }; - }); - - describe('with simple add function to test', () => { - before(() => { - sut = new JestTestRunner(testRunnerOptions); - return sut.init(); - }); - - it('should report completed tests', () => { - return expect(sut.run()).to.eventually.satisfy((runResult: RunResult) => { - expectToHaveSuccessfulTests(runResult, 5); - expectToHaveFailedTests(runResult, []); - expect(runResult.status).to.be.eq(RunStatus.Complete); - expect(runResult.errorMessages).to.have.length(0); - return true; - }); - }); - - it('should be able to run twice in quick succession', - () => expect(sut.run().then(() => sut.run())).to.eventually.have.property('status', RunStatus.Complete)); - }); - }); - - describe('when some tests fail', () => { - - before(() => { - const testRunnerOptions = { - files: [ - file({ name: 'testResources/sampleProject/src/Add.js', mutated: true }), - file({ name: 'testResources/sampleProject/src/__tests__/AddSpec.js', mutated: false }), - file({ name: 'testResources/sampleProject/src/__tests__/AddFailedSpec.js', mutated: false })], - port: 9878, - strykerOptions: { logLevel: 'trace' } - }; - sut = new JestTestRunner(testRunnerOptions); - return sut.init(); - }); - - it('should report failed tests', () => { - return expect(sut.run()).to.eventually.satisfy((runResult: RunResult) => { - expectToHaveSuccessfulTests(runResult, 5); - expectToHaveFailedTests(runResult, [ - // There seems no way to disable colored output in Jest, so color codes must be included here. - 'Expected value to be (using ===):\n \u001b[32m8\u001b[39m\nReceived:\n \u001b[31m7\u001b[39m\n', - 'Expected value to be (using ===):\n \u001b[32m4\u001b[39m\nReceived:\n \u001b[31m3\u001b[39m\n' - ]); - expect(runResult.status).to.be.eq(RunStatus.Complete); - return true; - }); - }); - }); - - describe('when an error occurs while running tests', () => { - - before(() => { - const testRunnerOptions = { - files: [ - file({ name: 'testResources/sampleProject/src/Error.js', mutated: true }), - file({ name: 'testResources/sampleProject/src/__tests__/ErrorSpec.js', mutated: true })], - port: 9879, - strykerOptions: { logLevel: 'trace' } - }; - sut = new JestTestRunner(testRunnerOptions); - return sut.init(); - }); - - it('should report Error with the error message', () => { - return expect(sut.run()).to.eventually.satisfy((runResult: RunResult) => { - expectToHaveSuccessfulTests(runResult, 0); - expectToHaveFailedTests(runResult, []); - expect(runResult.status).to.be.eq(RunStatus.Error); - expect(runResult.errorMessages.length).to.equal(1); - expect(runResult.errorMessages[0]).to.contain('ReferenceError: someGlobalVariableThatIsNotDeclared is not defined'); - return true; - }); - }); - }); - - describe('when no error occurred and no test is performed', () => { - - before(() => { - const testRunnerOptions = { - files: [ - file({ name: 'testResources/sampleProject/src/Add.js', mutated: true }), - file({ name: 'testResources/sampleProject/src/__tests__/EmptySpec.js', mutated: true })], - port: 9880, - strykerOptions: {} - }; - sut = new JestTestRunner(testRunnerOptions); - return sut.init(); - }); - - it('should report Complete without errors', () => { - return expect(sut.run()).to.eventually.satisfy((runResult: RunResult) => { - expectToHaveSuccessfulTests(runResult, 0); - - expectToHaveFailedTests(runResult, []); - expect(runResult.status).to.be.eq(RunStatus.Complete); - expect(runResult.errorMessages.length).to.equal(0); - return true; - }); - }); - }); - - describe('when adding an error file with included: false', () => { - - before(() => { - const testRunnerOptions = { - files: [ - file({ name: 'testResources/sampleProject/src/Add.js', mutated: true }), - file({ name: 'testResources/sampleProject/src/__tests__/AddSpec.js', mutated: false }), - file({ name: 'testResources/sampleProject/src/Error.js', mutated: false, included: false })], - port: 9881, - strykerOptions: {} - }; - sut = new JestTestRunner(testRunnerOptions); - return sut.init(); - }); - - it('should report Complete without errors', () => { - return expect(sut.run()).to.eventually.satisfy((runResult: RunResult) => { - expect(runResult.status).to.be.eq(RunStatus.Complete); - return true; - }); - }); - }); - - describe('when testing roots option project (jest^19)', () => { - before(() => { - const testRunnerOptions = { - files: [ - file({ name: 'testResources/roots-option-project/lib/isAdult.js', mutated: true, included: false }), - file({ name: 'testResources/roots-option-project/__tests__/isAdult.spec.js', mutated: false }) - ], - port: 9882, - strykerOptions: {} - }; - sut = new JestTestRunner(testRunnerOptions); - return sut.init(); - }); - - it('should report Complete without errors', () => { - return expect(sut.run()).to.eventually.satisfy((runResult: RunResult) => { - expect(runResult.errorMessages.join(',')).eq(''); - expect(runResult.status).to.be.eq(RunStatus.Complete); - return true; - }); - }); - }); - -}); diff --git a/test/integration/StrykerJestRunnerSpec.ts b/test/integration/StrykerJestRunnerSpec.ts new file mode 100644 index 0000000..eb868ae --- /dev/null +++ b/test/integration/StrykerJestRunnerSpec.ts @@ -0,0 +1,86 @@ +import JestConfigEditor from '../../src/JestConfigEditor'; +import { Config } from 'stryker-api/config'; +import { RunnerOptions, RunStatus, TestStatus } from 'stryker-api/test_runner'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import JestTestRunner from '../../src/JestTestRunner'; +import * as path from 'path'; + +describe('Integration StrykerJestRunner', () => { + const jestTestRunnerRoot = process.cwd(); + const reactProjectRoot = path.join(jestTestRunnerRoot, 'testResources', 'reactProject'); + const exampleProjectRoot = path.join(jestTestRunnerRoot, 'testResources', 'exampleProject'); + + let jestConfigEditor: JestConfigEditor; + let runOptions: RunnerOptions; + let getProjectRootStub: sinon.SinonStub; + + let sandbox: sinon.SinonSandbox; + + process.env.BABEL_ENV = 'test'; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + getProjectRootStub = sandbox.stub(process, 'cwd'); + + jestConfigEditor = new JestConfigEditor(); + + runOptions = { + files: [], + port: 0, + strykerOptions: new Config + }; + }); + + afterEach(() => sandbox.restore()); + + it('should run tests on the example react project', async () => { + getProjectRootStub.returns(reactProjectRoot); + runOptions.strykerOptions.set({ jest: { project: 'react' } }); + + jestConfigEditor.edit(runOptions.strykerOptions as Config); + + const jestTestRunner = new JestTestRunner(runOptions); + + const result = await jestTestRunner.run(); + + expect(result).to.have.property('tests'); + expect(result.tests).to.be.an('array').that.is.not.empty; + expect(result.tests[0].name).to.equal('renders without crashing'); + expect(result.tests[0].status).to.equal(TestStatus.Success); + expect(result.tests[0].timeSpentMs).to.be.above(0); + expect(result.tests[0].failureMessages).to.be.an('array').that.is.empty; + expect(result.status).to.equal(RunStatus.Complete); + }).timeout(10000); + + it('should run tests on the example custom project', async () => { + const testNames = [ + 'Add should be able to add two numbers', + 'Add should be able to add one to a number', + 'Add should be able negate a number', + 'Add should be able to recognize a negative number', + 'Add should be able to recognize that 0 is not a negative number', + 'Circle should have a circumference of 2PI when the radius is 1' + ]; + + getProjectRootStub.returns(exampleProjectRoot); + + jestConfigEditor.edit(runOptions.strykerOptions as Config); + const jestTestRunner = new JestTestRunner(runOptions); + + const result = await jestTestRunner.run(); + + expect(result).to.have.property('tests'); + expect(result.tests).to.be.an('array').with.length(6); + + for (let test of result.tests) { + expect(testNames).to.include(test.name); + expect(test.status).to.equal(TestStatus.Success); + expect(test.timeSpentMs).to.be.above(-1); + expect(test.failureMessages).to.be.an('array').that.is.empty; + } + + expect(result.status).to.equal(RunStatus.Complete); + }); +}); \ No newline at end of file diff --git a/test/unit/JestConfigEditorSpec.ts b/test/unit/JestConfigEditorSpec.ts new file mode 100644 index 0000000..f3caf60 --- /dev/null +++ b/test/unit/JestConfigEditorSpec.ts @@ -0,0 +1,56 @@ +import { Config } from 'stryker-api/config'; +import * as sinon from 'sinon'; +import { assert, expect } from 'chai'; +import JestConfigEditor from '../../src/JestConfigEditor'; +import DefaultJestConfigLoader, * as defaultJestConfigLoader from '../../src/configLoaders/DefaultJestConfigLoader'; +import ReactScriptsJestConfigLoader, * as reactScriptsJestConfigLoader from '../../src/configLoaders/ReactScriptsJestConfigLoader'; + +describe('JestConfigEditor', () => { + let jestConfigEditor: JestConfigEditor; + let sandbox: sinon.SinonSandbox; + + let defaultConfigLoaderStub: ConfigLoaderStub; + let reactConfigLoaderStub: ConfigLoaderStub; + let config: Config; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + defaultConfigLoaderStub = sinon.createStubInstance(DefaultJestConfigLoader); + reactConfigLoaderStub = sinon.createStubInstance(ReactScriptsJestConfigLoader); + + sandbox.stub(defaultJestConfigLoader, 'default').returns(defaultConfigLoaderStub); + sandbox.stub(reactScriptsJestConfigLoader, 'default').returns(reactConfigLoaderStub); + + jestConfigEditor = new JestConfigEditor(); + config = new Config(); + }); + + afterEach(() => sandbox.restore()); + + it('should call the defaultConfigLoader loadConfig method when no project is defined', () => { + jestConfigEditor.edit(config); + + expect(config.jest.project).to.equal('default'); + assert(defaultConfigLoaderStub.loadConfig.calledOnce, 'DefaultConfigLoader loadConfig not called'); + }); + + it('should call the reactConfigLoader loadConfig method when no project is defined', () => { + config.set({ jest: { project: 'react' } }); + + jestConfigEditor.edit(config); + + assert(reactConfigLoaderStub.loadConfig.calledOnce, 'ReactConfigLoader loadConfig not called'); + }); + + it('should return an error when an invalid project is defined', () => { + const project = 'invalidProject'; + config.set({ jest: { project } }); + + expect(() => jestConfigEditor.edit(config)).to.throw(Error, `No configLoader available for ${project}`); + }); +}); + +interface ConfigLoaderStub { + loadConfig: sinon.SinonStub; +} \ No newline at end of file diff --git a/test/unit/JestTestRunnerSpec.ts b/test/unit/JestTestRunnerSpec.ts new file mode 100644 index 0000000..6a5767c --- /dev/null +++ b/test/unit/JestTestRunnerSpec.ts @@ -0,0 +1,117 @@ +import JestTestAdapterFactory from '../../src/jestTestAdapters/JestTestAdapterFactory'; +import JestTestRunner from '../../src/JestTestRunner'; +import { Config } from 'stryker-api/config'; +import * as fakeResults from '../helpers/testResultProducer'; +import * as sinon from 'sinon'; +import { assert, expect } from 'chai'; +import { RunStatus, TestStatus } from 'stryker-api/test_runner'; + +describe('JestTestRunner', () => { + let sandbox: sinon.SinonSandbox; + let jestTestAdapterFactoryStub: sinon.SinonStub; + let runJestStub: sinon.SinonStub; + + let strykerOptions: Config; + let jestTestRunner: JestTestRunner; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + runJestStub = sinon.stub(); + runJestStub.resolves({ results: { testResults: [] }}); + + strykerOptions = new Config; + strykerOptions.set({ jest: { config: { property: 'value' }}}); + + jestTestRunner = new JestTestRunner({ + files: [], + port: 0, + strykerOptions + }); + + jestTestAdapterFactoryStub = sandbox.stub(JestTestAdapterFactory, 'getJestTestAdapter'); + jestTestAdapterFactoryStub.returns({ + run: runJestStub + }); + }); + + afterEach(() => sandbox.restore()); + + it('should call jestTestAdapterFactory "getJestTestAdapter" method to obtain a testRunner', async () => { + await jestTestRunner.run(); + + assert(jestTestAdapterFactoryStub.called); + }); + + it('should call the run function with the provided config and the projectRoot', async () => { + await jestTestRunner.run(); + + assert(runJestStub.called); + }); + + it('should call the jestTestRunner run method and return a correct runResult', async () => { + runJestStub.resolves({ results: fakeResults.createSuccessResult() }); + + const result = await jestTestRunner.run(); + + expect(result).to.deep.equal({ + status: RunStatus.Complete, + tests: [ + { + name: 'App renders without crashing', + status: TestStatus.Success, + timeSpentMs: 23, + failureMessages: [] + } + ], + errorMessages: [] + }); + }); + + it('should call the jestTestRunner run method and return a negative runResult', async () => { + runJestStub.resolves({ results: fakeResults.createFailResult() }); + + const result = await jestTestRunner.run(); + + expect(result).to.deep.equal({ + status: RunStatus.Complete, + tests: [{ + name: 'App render renders without crashing', + status: TestStatus.Failed, + timeSpentMs: 2, + failureMessages: [ + 'Fail message 1', + 'Fail message 2' + ] + }, + { + name: 'App render renders without crashing', + status: TestStatus.Failed, + timeSpentMs: 0, + failureMessages: [ + 'Fail message 3', + 'Fail message 4' + ] + }, + { + name: 'App renders without crashing', + status: TestStatus.Success, + timeSpentMs: 23, + failureMessages: [] + }], + errorMessages: ['test failed - App.test.js'] + }); + }); + + it('should return an error result when a runtime error occurs', async () => { + runJestStub.resolves({ results: { testResults: [], numRuntimeErrorTestSuites: 1 }}); + + const result = await jestTestRunner.run(); + + expect(result).to.deep.equal({ + status: RunStatus.Error, + tests: [], + errorMessages: [] + }); + }); +}); \ No newline at end of file diff --git a/test/unit/configLoaders/DefaultConfigLoaderSpec.ts b/test/unit/configLoaders/DefaultConfigLoaderSpec.ts new file mode 100644 index 0000000..13ac4d8 --- /dev/null +++ b/test/unit/configLoaders/DefaultConfigLoaderSpec.ts @@ -0,0 +1,43 @@ +import DefaultJestConfigLoader from '../../../src/configLoaders/DefaultJestConfigLoader'; +import * as sinon from 'sinon'; +import { expect, assert } from 'chai'; +import * as path from 'path'; +import * as fs from 'fs'; + +describe('DefaultJestConfigLoader', () => { + let defaultConfigLoader: DefaultJestConfigLoader; + let projectRoot: string = '/path/to/project/root'; + let fsStub: FsStub = {}; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + fsStub.readFileSync = sandbox.stub(fs, 'readFileSync'); + + defaultConfigLoader = new DefaultJestConfigLoader(projectRoot, fs); + }); + + afterEach(() => sandbox.restore()); + + it('should load the Jest configuration from the package.json in the project', () => { + fsStub.readFileSync.returns('{ "jest": { "exampleProperty": "exampleValue" }}'); + + const config = defaultConfigLoader.loadConfig(); + + assert(fsStub.readFileSync.calledWith(path.join(projectRoot, 'package.json'), 'utf8'), 'readFileSync not called with projectRoot'); + expect(config).to.deep.equal({ + exampleProperty: 'exampleValue' + }); + }); + + it('should return an error when no Jest configuration is specified in the config.json', () => { + fsStub.readFileSync.returns('{}'); + + expect(() => defaultConfigLoader.loadConfig()).to.throw(Error, 'No Jest configuration found in your package.json'); + }); +}); + +interface FsStub { + [key: string]: sinon.SinonStub; +} \ No newline at end of file diff --git a/test/unit/configLoaders/ReactConfigLoaderSpec.ts b/test/unit/configLoaders/ReactConfigLoaderSpec.ts new file mode 100644 index 0000000..5da1a4a --- /dev/null +++ b/test/unit/configLoaders/ReactConfigLoaderSpec.ts @@ -0,0 +1,54 @@ +import * as path from 'path'; +import * as sinon from 'sinon'; +import { assert, expect } from 'chai'; +import ReactScriptsJestConfigLoader from '../../../src/configLoaders/ReactScriptsJestConfigLoader'; +import * as createReactJestConfig from '../../../src/utils/createReactJestConfig'; + +const fakeRequire: any = { + resolve: () => { } +}; + +describe('ReactScriptsJestConfigLoader', () => { + let reactConfigLoader: ReactScriptsJestConfigLoader; + let sandbox: sinon.SinonSandbox; + let requireResolveStub: sinon.SinonStub; + let createReactJestConfigStub: sinon.SinonStub; + + let projectRoot = '/path/to/project'; + let reactScriptsPackagePath = './node_modules/react-scripts/package.json'; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + createReactJestConfigStub = sandbox.stub(createReactJestConfig, 'default'); + createReactJestConfigStub.callsFake((resolve: any, projectRoot: string, eject: boolean) => ({ + relativePath: resolve('test'), + projectRoot, + eject + })); + + requireResolveStub = sandbox.stub(fakeRequire, 'resolve'); + requireResolveStub.returns(reactScriptsPackagePath); + + reactConfigLoader = new ReactScriptsJestConfigLoader(projectRoot, fakeRequire); + }); + + afterEach(() => sandbox.restore()); + + it('should load the configuration via the createJestConfig method provided by react-scripts', () => { + reactConfigLoader.loadConfig(); + + assert(requireResolveStub.calledWith('react-scripts/package.json')); + }); + + it('should generate a configuration', () => { + const config = reactConfigLoader.loadConfig(); + + expect(config).to.deep.equal({ + relativePath: path.join('node_modules', 'react-scripts', 'test'), + projectRoot: '/path/to/project', + eject: false, + testEnvironment: 'jsdom' + }); + }); +}); \ No newline at end of file diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts deleted file mode 100644 index 817c1bf..0000000 --- a/test/unit/index.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as sinon from 'sinon'; -import { TestRunnerFactory } from 'stryker-api/test_runner'; -import { TestFrameworkFactory } from 'stryker-api/test_framework'; -import { ConfigEditorFactory } from 'stryker-api/config'; -import { ReporterFactory } from 'stryker-api/report'; -import JestTestRunner from '../../src/JestTestRunner'; -import { expect } from 'chai'; -import * as path from 'path'; - -describe('index', () => { - let sandbox: sinon.SinonSandbox; - - const mockFactory = () => ({ register: sinon.stub() }); - let testRunnerFactoryMock: any; - let testFrameworkFactoryMock: any; - let reporterFactoryMock: any; - let configEditorFactoryMock: any; - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - testRunnerFactoryMock = mockFactory(); - testFrameworkFactoryMock = mockFactory(); - reporterFactoryMock = mockFactory(); - configEditorFactoryMock = mockFactory(); - - sandbox.stub(TestFrameworkFactory, 'instance').returns(testFrameworkFactoryMock); - sandbox.stub(TestRunnerFactory, 'instance').returns(testRunnerFactoryMock); - sandbox.stub(ReporterFactory, 'instance').returns(reporterFactoryMock); - sandbox.stub(ConfigEditorFactory, 'instance').returns(configEditorFactoryMock); - - // Do not import the `index` file es6 style, because we need to - // make sure it is re-imported every time. - const indexPath = path.resolve('./src/index.js'); - delete require.cache[indexPath]; - require('../../src/index'); - }); - - it('should register the JestTestRunner', () => - expect(testRunnerFactoryMock.register).to.have.been.calledWith('jest', JestTestRunner)); - - afterEach(() => sandbox.restore()); -}); diff --git a/test/unit/jestTestAdapters/JestCallbackTestAdapterSpec.ts b/test/unit/jestTestAdapters/JestCallbackTestAdapterSpec.ts new file mode 100644 index 0000000..ea4bf1f --- /dev/null +++ b/test/unit/jestTestAdapters/JestCallbackTestAdapterSpec.ts @@ -0,0 +1,62 @@ +import JestCallbackTestAdapter from '../../../src/jestTestAdapters/JestCallbackTestAdapter'; +import * as sinon from 'sinon'; +import { expect, assert } from 'chai'; + +const loader: any = { + require: () => { } +}; + +describe('JestCallbackTestAdapter', () => { + let sandbox: sinon.SinonSandbox; + let runCLIStub: sinon.SinonStub; + let requireStub: sinon.SinonStub; + + let jestCallbackTestAdapter: JestCallbackTestAdapter; + + const projectRoot = '/path/to/project'; + const jestConfig: any = { rootDir: projectRoot }; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + runCLIStub = sinon.stub(); + runCLIStub.callsArgWith(2, 'testResult'); + + requireStub = sandbox.stub(loader, 'require'); + requireStub.returns({ + runCLI: runCLIStub + }); + + jestCallbackTestAdapter = new JestCallbackTestAdapter(loader.require); + }); + + afterEach(() => sandbox.restore()); + + it('should require jest when the constructor is called', () => { + assert(requireStub.calledWith('jest'), 'require not called with jest'); + }); + + it('should set reporters to an empty array', async () => { + await jestCallbackTestAdapter.run(jestConfig, projectRoot); + + expect(jestConfig.reporters).to.be.an('array').that.is.empty; + }); + + it('should call the runCLI method with the correct projectRoot', async () => { + await jestCallbackTestAdapter.run(jestConfig, projectRoot); + + assert(runCLIStub.calledWith({ + config: JSON.stringify({ rootDir: projectRoot, reporters: [] }), + runInBand: true, + silent: true + }, [projectRoot])); + }); + + it('should call the runCLI method and return the test result', async () => { + const result = await jestCallbackTestAdapter.run(jestConfig, projectRoot); + + expect(result).to.deep.equal({ + results: 'testResult' + }); + }); +}); \ No newline at end of file diff --git a/test/unit/jestTestAdapters/JestPromiseTestAdapterSpec.ts b/test/unit/jestTestAdapters/JestPromiseTestAdapterSpec.ts new file mode 100644 index 0000000..9330a6e --- /dev/null +++ b/test/unit/jestTestAdapters/JestPromiseTestAdapterSpec.ts @@ -0,0 +1,70 @@ +import JestPromiseTestAdapter from '../../../src/jestTestAdapters/JestPromiseTestAdapter'; +import * as sinon from 'sinon'; +import { expect, assert } from 'chai'; + +const loader: any = { + require: () => { } +}; + +describe('JestPromiseTestAdapter', () => { + let sandbox: sinon.SinonSandbox; + let runCLIStub: sinon.SinonStub; + let requireStub: sinon.SinonStub; + + let jestPromiseTestAdapter: JestPromiseTestAdapter; + + const projectRoot = '/path/to/project'; + const jestConfig: any = { rootDir: projectRoot }; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + runCLIStub = sinon.stub(); + runCLIStub.callsFake((config: Object, projectRootArray: Array) => Promise.resolve({ + result: 'testResult', + config: config + })); + + requireStub = sandbox.stub(loader, 'require'); + requireStub.returns({ + runCLI: runCLIStub + }); + + jestPromiseTestAdapter = new JestPromiseTestAdapter(loader.require); + }); + + afterEach(() => sandbox.restore()); + + it('should require jest when the constructor is called', () => { + assert(requireStub.calledWith('jest'), 'require not called with jest'); + }); + + it('should set reporters to an empty array', async () => { + await jestPromiseTestAdapter.run(jestConfig, projectRoot); + + expect(jestConfig.reporters).to.be.an('array').that.is.empty; + }); + + it('should call the runCLI method with the correct projectRoot', async () => { + await jestPromiseTestAdapter.run(jestConfig, projectRoot); + + assert(runCLIStub.calledWith({ + config: JSON.stringify({ rootDir: projectRoot, reporters: [] }), + runInBand: true, + silent: true + }, [projectRoot])); + }); + + it('should call the runCLI method and return the test result', async () => { + const result = await jestPromiseTestAdapter.run(jestConfig, projectRoot); + + expect(result).to.deep.equal({ + result: 'testResult', + config: { + config: JSON.stringify({ rootDir: projectRoot, reporters: [] }), + runInBand: true, + silent: true + } + }); + }); +}); \ No newline at end of file diff --git a/test/unit/jestTestAdapters/JestTestAdapterFactorySpec.ts b/test/unit/jestTestAdapters/JestTestAdapterFactorySpec.ts new file mode 100644 index 0000000..2da606c --- /dev/null +++ b/test/unit/jestTestAdapters/JestTestAdapterFactorySpec.ts @@ -0,0 +1,64 @@ +import JestTestAdapterFactory from '../../../src/jestTestAdapters/JestTestAdapterFactory'; +import JestPromiseTestAdapter, * as jestPromiseTestAdapter from '../../../src/jestTestAdapters/JestPromiseTestAdapter'; +import JestCallbackTestAdapter, * as jestCallbackTestAdapter from '../../../src/jestTestAdapters/JestCallbackTestAdapter'; +import * as sinon from 'sinon'; +import { expect, assert } from 'chai'; + +const loader: any = { + require: () => {} +}; + +describe('JestTestAdapterFactory', () => { + let sandbox: sinon.SinonSandbox; + let jestPromiseTestAdapterStub: TestAdapterStub; + let jestCallbackTestAdapterStub: TestAdapterStub; + let requireStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + jestPromiseTestAdapterStub = sinon.createStubInstance(JestPromiseTestAdapter); + jestCallbackTestAdapterStub = sinon.createStubInstance(JestCallbackTestAdapter); + + sandbox.stub(jestPromiseTestAdapter, 'default').returns(jestPromiseTestAdapterStub); + sandbox.stub(jestCallbackTestAdapter, 'default').returns(jestCallbackTestAdapterStub); + + requireStub = sandbox.stub(loader, 'require'); + }); + + afterEach(() => sandbox.restore()); + + it('should return a JestPromiseAdapter when the jest version is higher or equal to 21.0.0', () => { + requireStub.returns({ version: '21.0.0' }); + + const testAdapter = JestTestAdapterFactory.getJestTestAdapter(loader.require); + + expect(testAdapter).to.equal(jestPromiseTestAdapterStub); + }); + + it('should return a JestCallbackTestAdapter when the jest version is lower than 21.0.0 but higher or equal to 20.0.0', () => { + requireStub.returns({ version: '20.0.0' }); + + const testAdapter = JestTestAdapterFactory.getJestTestAdapter(loader.require); + + expect(testAdapter).to.equal(jestCallbackTestAdapterStub); + }); + + it('should load the jest package.json with require', () => { + requireStub.returns({ version: '20.0.0' }); + + JestTestAdapterFactory.getJestTestAdapter(loader.require); + + assert(requireStub.calledWith('jest/package.json'), 'require not called with "jest/package.json"'); + }); + + it('should throw an error when the jest version is lower than 20.0.0', () => { + requireStub.returns({ version: '19.0.0' }); + + expect(() => JestTestAdapterFactory.getJestTestAdapter(loader.require)).to.throw(Error, 'You need Jest version >= 20.0.0 to use Stryker'); + }); +}); + +interface TestAdapterStub { + run: sinon.SinonStub; +} \ No newline at end of file diff --git a/test/unit/utils/createReactJestConfig.ts b/test/unit/utils/createReactJestConfig.ts new file mode 100644 index 0000000..15e7cc1 --- /dev/null +++ b/test/unit/utils/createReactJestConfig.ts @@ -0,0 +1,32 @@ +import createReactJestConfig from '../../../src/utils/createReactJestConfig'; +import { expect, assert } from 'chai'; +import * as sinon from 'sinon'; + +describe('createReactJestConfig', () => { + let loaderStub: sinon.SinonStub; + let sandbox: sinon.SinonSandbox; + let loader: any = { + require: () => {} + }; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + loaderStub = sandbox.stub(loader, 'require'); + loaderStub.returns((resolve: Function, projectRoot: string, ejected: boolean) => { + return 'jestConfig'; + }); + }); + + afterEach(() => sandbox.restore()); + + it('should call the loader with the react jest config generator', () => { + createReactJestConfig(() => {}, '/path/to/project', false, loader.require); + + assert(loaderStub.calledWith('react-scripts/scripts/utils/createJestConfig')); + }); + + it('should return a jest config', () => { + expect(createReactJestConfig(() => {}, '/path/to/project', false, loader.require)).to.equal('jestConfig'); + }); +}); \ No newline at end of file diff --git a/testResources/exampleProject/package.json b/testResources/exampleProject/package.json new file mode 100644 index 0000000..24a701a --- /dev/null +++ b/testResources/exampleProject/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "name": "example-project", + "version": "0.0.0", + "description": "A testResource for jest-test-runner", + "jest": { + "collectCoverage": false, + "moduleFileExtensions": ["js", "json", "jsx", "node"], + "testEnvironment": "jest-environment-jsdom", + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.jsx?$", + "testRunner": "jest-jasmine2", + "verbose": true + } +} diff --git a/testResources/sampleProject/src/Add.js b/testResources/exampleProject/src/Add.js similarity index 99% rename from testResources/sampleProject/src/Add.js rename to testResources/exampleProject/src/Add.js index 9943131..58d562e 100644 --- a/testResources/sampleProject/src/Add.js +++ b/testResources/exampleProject/src/Add.js @@ -17,4 +17,4 @@ exports.isNegativeNumber = function(number) { isNegative = true; } return isNegative; -}; +}; \ No newline at end of file diff --git a/testResources/sampleProject/src/Circle.js b/testResources/exampleProject/src/Circle.js similarity index 98% rename from testResources/sampleProject/src/Circle.js rename to testResources/exampleProject/src/Circle.js index d33c864..616d486 100644 --- a/testResources/sampleProject/src/Circle.js +++ b/testResources/exampleProject/src/Circle.js @@ -5,4 +5,4 @@ exports.getCircumference = function(radius) { exports.untestedFunction = function() { var i = 5 / 2 * 3; -}; +}; \ No newline at end of file diff --git a/testResources/sampleProject/src/__tests__/AddSpec.js b/testResources/exampleProject/src/__tests__/AddSpec.js similarity index 79% rename from testResources/sampleProject/src/__tests__/AddSpec.js rename to testResources/exampleProject/src/__tests__/AddSpec.js index f410f05..c31f82d 100644 --- a/testResources/sampleProject/src/__tests__/AddSpec.js +++ b/testResources/exampleProject/src/__tests__/AddSpec.js @@ -1,4 +1,7 @@ -var { add, addOne, isNegativeNumber, negate } = require('../Add'); +var add = require('../Add').add; +var addOne = require('../Add').addOne; +var isNegativeNumber = require('../Add').isNegativeNumber; +var negate = require('../Add').negate; describe('Add', function() { it('should be able to add two numbers', function() { @@ -11,7 +14,7 @@ describe('Add', function() { expect(actual).toBe(expected); }); - it('should be able 1 to a number', function() { + it('should be able to add one to a number', function() { var number = 2; var expected = 3; @@ -44,4 +47,4 @@ describe('Add', function() { expect(isNegative).toBe(false); }); -}); +}); \ No newline at end of file diff --git a/testResources/sampleProject/src/__tests__/CircleSpec.js b/testResources/exampleProject/src/__tests__/CircleSpec.js similarity index 81% rename from testResources/sampleProject/src/__tests__/CircleSpec.js rename to testResources/exampleProject/src/__tests__/CircleSpec.js index 48631e4..86dc246 100644 --- a/testResources/sampleProject/src/__tests__/CircleSpec.js +++ b/testResources/exampleProject/src/__tests__/CircleSpec.js @@ -1,4 +1,4 @@ -var { getCircumference } = require('../Circle'); +var getCircumference = require('../Circle').getCircumference; describe('Circle', function() { it('should have a circumference of 2PI when the radius is 1', function() { @@ -9,4 +9,4 @@ describe('Circle', function() { expect(circumference).toBe(expectedCircumference); }); -}); +}); \ No newline at end of file diff --git a/testResources/reactProject/.gitignore b/testResources/reactProject/.gitignore new file mode 100644 index 0000000..d30f40e --- /dev/null +++ b/testResources/reactProject/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/testResources/reactProject/package.json b/testResources/reactProject/package.json new file mode 100644 index 0000000..dea8fd1 --- /dev/null +++ b/testResources/reactProject/package.json @@ -0,0 +1,16 @@ +{ + "name": "react-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^16.2.0", + "react-dom": "^16.2.0", + "react-scripts": "1.0.17" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} \ No newline at end of file diff --git a/testResources/reactProject/public/favicon.ico b/testResources/reactProject/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/testResources/reactProject/public/index.html b/testResources/reactProject/public/index.html new file mode 100644 index 0000000..ed0ebaf --- /dev/null +++ b/testResources/reactProject/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + React App + + + +
+ + + diff --git a/testResources/reactProject/public/manifest.json b/testResources/reactProject/public/manifest.json new file mode 100644 index 0000000..ef19ec2 --- /dev/null +++ b/testResources/reactProject/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/testResources/reactProject/src/App.css b/testResources/reactProject/src/App.css new file mode 100644 index 0000000..c5c6e8a --- /dev/null +++ b/testResources/reactProject/src/App.css @@ -0,0 +1,28 @@ +.App { + text-align: center; +} + +.App-logo { + animation: App-logo-spin infinite 20s linear; + height: 80px; +} + +.App-header { + background-color: #222; + height: 150px; + padding: 20px; + color: white; +} + +.App-title { + font-size: 1.5em; +} + +.App-intro { + font-size: large; +} + +@keyframes App-logo-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/testResources/reactProject/src/App.js b/testResources/reactProject/src/App.js new file mode 100644 index 0000000..203067e --- /dev/null +++ b/testResources/reactProject/src/App.js @@ -0,0 +1,21 @@ +import React, { Component } from 'react'; +import logo from './logo.svg'; +import './App.css'; + +class App extends Component { + render() { + return ( +
+
+ logo +

Welcome to React

+
+

+ To get started, edit src/App.js and save to reload. +

+
+ ); + } +} + +export default App; diff --git a/testResources/reactProject/src/App.test.js b/testResources/reactProject/src/App.test.js new file mode 100644 index 0000000..b84af98 --- /dev/null +++ b/testResources/reactProject/src/App.test.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); +}); diff --git a/testResources/reactProject/src/index.css b/testResources/reactProject/src/index.css new file mode 100644 index 0000000..b4cc725 --- /dev/null +++ b/testResources/reactProject/src/index.css @@ -0,0 +1,5 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} diff --git a/testResources/reactProject/src/index.js b/testResources/reactProject/src/index.js new file mode 100644 index 0000000..fae3e35 --- /dev/null +++ b/testResources/reactProject/src/index.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import registerServiceWorker from './registerServiceWorker'; + +ReactDOM.render(, document.getElementById('root')); +registerServiceWorker(); diff --git a/testResources/reactProject/src/logo.svg b/testResources/reactProject/src/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/testResources/reactProject/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/testResources/reactProject/src/registerServiceWorker.js b/testResources/reactProject/src/registerServiceWorker.js new file mode 100644 index 0000000..12542ba --- /dev/null +++ b/testResources/reactProject/src/registerServiceWorker.js @@ -0,0 +1,108 @@ +// In production, we register a service worker to serve assets from local cache. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on the "N+1" visit to a page, since previously +// cached resources are updated in the background. + +// To learn more about the benefits of this model, read https://goo.gl/KwvDNy. +// This link also includes instructions on opting out of this behavior. + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export default function register() { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Lets check if a service worker still exists or not. + checkValidServiceWorker(swUrl); + } else { + // Is not local host. Just register service worker + registerValidSW(swUrl); + } + }); + } +} + +function registerValidSW(swUrl) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the old content will have been purged and + // the fresh content will have been added to the cache. + // It's the perfect time to display a "New content is + // available; please refresh." message in your web app. + console.log('New content is available; please refresh.'); + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + if ( + response.status === 404 || + response.headers.get('content-type').indexOf('javascript') === -1 + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +} diff --git a/testResources/roots-option-project/__tests__/isAdult.spec.js b/testResources/roots-option-project/__tests__/isAdult.spec.js deleted file mode 100644 index 49cb1e1..0000000 --- a/testResources/roots-option-project/__tests__/isAdult.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -const isAdult = require('../lib/isAdult.js'); - -it('above 18', () => { - if(!isAdult(19)){ - throw new Error('Test failed'); - } -}); \ No newline at end of file diff --git a/testResources/roots-option-project/lib/isAdult.js b/testResources/roots-option-project/lib/isAdult.js deleted file mode 100644 index 1540730..0000000 --- a/testResources/roots-option-project/lib/isAdult.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function add(age) { - return age >= 18; -}; \ No newline at end of file diff --git a/testResources/sampleProject/src/Error.js b/testResources/sampleProject/src/Error.js deleted file mode 100644 index c29bead..0000000 --- a/testResources/sampleProject/src/Error.js +++ /dev/null @@ -1,2 +0,0 @@ - -someGlobalVariableThatIsNotDeclared.someMethod(''); diff --git a/testResources/sampleProject/src/InfiniteAdd.js b/testResources/sampleProject/src/InfiniteAdd.js deleted file mode 100644 index 5c6e80c..0000000 --- a/testResources/sampleProject/src/InfiniteAdd.js +++ /dev/null @@ -1,7 +0,0 @@ -var add = function(num1, num2) { - var result; - while(true){ - result = num1 + num2; - } - return result; -}; diff --git a/testResources/sampleProject/src/__tests__/AddFailedSpec.js b/testResources/sampleProject/src/__tests__/AddFailedSpec.js deleted file mode 100644 index a4b6ea3..0000000 --- a/testResources/sampleProject/src/__tests__/AddFailedSpec.js +++ /dev/null @@ -1,22 +0,0 @@ -var { add, addOne } = require('../Add'); - -describe('Add', function() { - it('should be able to add two numbers and add one', function() { - var num1 = 2; - var num2 = 5; - var expected = num1 + num2 + 1; - - var actual = add(num1, num2); - - expect(actual).toBe(expected); - }); - - it('should be to add able 1 to a number and actually add 2', function() { - var num = 2; - var expected = 4; - - var actual = addOne(num); - - expect(actual).toBe(expected); - }); -}); diff --git a/testResources/sampleProject/src/__tests__/EmptySpec.js b/testResources/sampleProject/src/__tests__/EmptySpec.js deleted file mode 100644 index e69de29..0000000 diff --git a/testResources/sampleProject/src/__tests__/ErrorSpec.js b/testResources/sampleProject/src/__tests__/ErrorSpec.js deleted file mode 100644 index f9c2177..0000000 --- a/testResources/sampleProject/src/__tests__/ErrorSpec.js +++ /dev/null @@ -1,7 +0,0 @@ -var x = require('../Error'); - -describe('Error', function() { - it('ignorable spec', function() { - expect(true).toBe(true); - }); -}); \ No newline at end of file diff --git a/testResources/sampleProject/src/__tests__/FailingAddSpec.js b/testResources/sampleProject/src/__tests__/FailingAddSpec.js deleted file mode 100644 index 944f7b7..0000000 --- a/testResources/sampleProject/src/__tests__/FailingAddSpec.js +++ /dev/null @@ -1,13 +0,0 @@ -var { add } = require('../Add'); - -describe('Add', function() { - it('this test should fail', function() { - var num1 = 2; - var num2 = 5; - var expected = 0; - - var actual = add(num1, num2); - - expect(actual).toBe(expected); - }); -}); diff --git a/testResources/sampleProject/stryker.conf.js b/testResources/sampleProject/stryker.conf.js deleted file mode 100644 index 7645dde..0000000 --- a/testResources/sampleProject/stryker.conf.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = function(config){ - config.set({ - files: ['testResources/sampleProject/src/*.js', 'testResources/sampleProject/src/__tests__/*.js'], - mutate: ['testResources/sampleProject/src/*.js'], - testFramework: 'jasmine', - testRunner: 'jest' - }); -}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e0a698f..c995ea4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,22 +6,17 @@ "moduleResolution": "node", "rootDir": ".", "sourceMap": true, - "removeComments": false, "declaration": true, "forceConsistentCasingInFileNames": true, "allowJs": false, + "importHelpers": true, "noUnusedLocals": true, "noImplicitReturns": true, + "strictNullChecks": true, "lib": [ "es5", "es2015.promise", "es2015.core" ] - }, - "exclude": [ - "node_modules", - "testResources", - "**/*.d.ts", - "*.d.ts" - ] -} + } +} \ No newline at end of file diff --git a/tslint.json b/tslint.json index c628ee2..c18db6f 100644 --- a/tslint.json +++ b/tslint.json @@ -1,58 +1,58 @@ { - "rules": { - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "indent": [ - true, - "spaces" - ], - "no-duplicate-variable": true, - "no-eval": true, - "no-internal-module": true, - "no-trailing-whitespace": false, - "no-unsafe-finally": true, - "no-var-keyword": true, - "one-line": [ - true, - "check-open-brace", - "check-whitespace" - ], - "quotemark": [ - true, - "single" - ], - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "variable-name": [ - true, - "ban-keywords" - ], - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ] - } + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "indent": [ + true, + "spaces" + ], + "no-duplicate-variable": true, + "no-eval": true, + "no-internal-module": true, + "no-trailing-whitespace": false, + "no-unsafe-finally": true, + "no-var-keyword": true, + "one-line": [ + true, + "check-open-brace", + "check-whitespace" + ], + "quotemark": [ + true, + "single" + ], + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": [ + true, + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } } \ No newline at end of file