From 5673e6b562e68ddb2e190dbb5092d696b0fcee53 Mon Sep 17 00:00:00 2001 From: Wmaarts Date: Tue, 27 Nov 2018 12:30:19 +0100 Subject: [PATCH] feat(Stryker CLI 'init'): Support for preset configuration during 'stryker init' (#1248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a question to the `init` command: ``` Are you using one of these frameworks? Then select a preset configuration. (Use arrow keys) > angular-cli react vueJs ────────────── None/other ``` If you choose a preset, preset specific questions may be asked. If all is well, Stryker will work out-of-the box. --- .../src/initializer/StrykerConfigWriter.ts | 29 +++- .../src/initializer/StrykerInitializer.ts | 64 ++++++--- .../src/initializer/StrykerInquirer.ts | 14 ++ .../stryker/src/initializer/StrykerPresets.ts | 8 ++ .../src/initializer/presets/AngularPreset.ts | 39 +++++ .../stryker/src/initializer/presets/Preset.ts | 7 + .../presets/PresetConfiguration.ts | 5 + .../src/initializer/presets/ReactPreset.ts | 60 ++++++++ .../src/initializer/presets/VueJsPreset.ts | 106 ++++++++++++++ packages/stryker/stryker.conf.js | 125 ++++++++-------- .../test/unit/initializer/PresetsSpec.ts | 135 ++++++++++++++++++ .../initializer/StrykerInitializerSpec.ts | 108 +++++++++++++- 12 files changed, 605 insertions(+), 95 deletions(-) create mode 100644 packages/stryker/src/initializer/StrykerPresets.ts create mode 100644 packages/stryker/src/initializer/presets/AngularPreset.ts create mode 100644 packages/stryker/src/initializer/presets/Preset.ts create mode 100644 packages/stryker/src/initializer/presets/PresetConfiguration.ts create mode 100644 packages/stryker/src/initializer/presets/ReactPreset.ts create mode 100644 packages/stryker/src/initializer/presets/VueJsPreset.ts create mode 100644 packages/stryker/test/unit/initializer/PresetsSpec.ts diff --git a/packages/stryker/src/initializer/StrykerConfigWriter.ts b/packages/stryker/src/initializer/StrykerConfigWriter.ts index b06e2c22cb..a147887044 100644 --- a/packages/stryker/src/initializer/StrykerConfigWriter.ts +++ b/packages/stryker/src/initializer/StrykerConfigWriter.ts @@ -4,6 +4,7 @@ import { getLogger } from 'stryker-api/logging'; import { StrykerOptions } from 'stryker-api/core'; import PromptOption from './PromptOption'; import { format } from 'prettier'; +import PresetConfiguration from './presets/PresetConfiguration'; const STRYKER_CONFIG_FILE = 'stryker.conf.js'; @@ -47,6 +48,15 @@ export default class StrykerConfigWriter { return this.writeStrykerConfig(configObject); } + /** + * Create stryker.conf.js based on the chosen preset + * @function + */ + public async writePreset(presetConfig: PresetConfiguration) { + return this.writeStrykerConfigRaw(presetConfig.config, `// This config was generated using a preset. + // Please see the handbook for more information: ${presetConfig.handbookUrl}`); + } + private configureTestFramework(configObject: Partial, selectedTestFramework: null | PromptOption) { if (selectedTestFramework) { configObject.testFramework = selectedTestFramework.name; @@ -56,17 +66,22 @@ export default class StrykerConfigWriter { } } - private writeStrykerConfig(configObject: Partial) { + private writeStrykerConfigRaw(rawConfig: string, rawHeader = '') { this.out('Writing stryker.conf.js...'); - return fsAsPromised.writeFile(STRYKER_CONFIG_FILE, this.wrapInModule(configObject)); - } - - private wrapInModule(configObject: Partial) { - return format(` + const formattedConf = format(`${rawHeader} module.exports = function(config){ config.set( - ${JSON.stringify(configObject, null, 2)} + ${rawConfig} ); }`, { parser: 'babylon' }); + return fsAsPromised.writeFile(STRYKER_CONFIG_FILE, formattedConf); + } + + private writeStrykerConfig(configObject: Partial) { + return this.writeStrykerConfigRaw(this.wrapInModule(configObject)); + } + + private wrapInModule(configObject: Partial): string { + return JSON.stringify(configObject, null, 2); } } diff --git a/packages/stryker/src/initializer/StrykerInitializer.ts b/packages/stryker/src/initializer/StrykerInitializer.ts index 5ec668af4e..c25f29d428 100644 --- a/packages/stryker/src/initializer/StrykerInitializer.ts +++ b/packages/stryker/src/initializer/StrykerInitializer.ts @@ -6,6 +6,8 @@ import { getLogger } from 'stryker-api/logging'; import { filterEmpty } from '../utils/objectUtils'; import StrykerConfigWriter from './StrykerConfigWriter'; import CommandTestRunner from '../test-runner/CommandTestRunner'; +import StrykerPresets from './StrykerPresets'; +import Preset from './presets/Preset'; const enum PackageManager { Npm = 'npm', @@ -17,7 +19,7 @@ export default class StrykerInitializer { private readonly log = getLogger(StrykerInitializer.name); private readonly inquirer = new StrykerInquirer(); - constructor(private readonly out = console.log, private readonly client: NpmClient = new NpmClient()) { } + constructor(private readonly out = console.log, private readonly client: NpmClient = new NpmClient(), private readonly strykerPresets: Preset[] = StrykerPresets) { } /** * Runs the initializer will prompt the user for questions about his setup. After that, install plugins and configure Stryker. @@ -27,6 +29,50 @@ export default class StrykerInitializer { const configWriter = new StrykerConfigWriter(this.out); configWriter.guardForExistingConfig(); this.patchProxies(); + const selectedPreset = await this.selectPreset(); + if (selectedPreset) { + await this.initiatePreset(configWriter, selectedPreset); + } + else { + await this.initiateCustom(configWriter); + } + this.out('Done configuring stryker. Please review `stryker.conf.js`, you might need to configure transpilers or your test runner correctly.'); + this.out('Let\'s kill some mutants with this command: `stryker run`'); + } + + /** + * The typed rest client works only with the specific HTTP_PROXY and HTTPS_PROXY env settings. + * Let's make sure they are available. + */ + private patchProxies() { + const copyEnvVariable = (from: string, to: string) => { + if (process.env[from] && !process.env[to]) { + process.env[to] = process.env[from]; + } + }; + copyEnvVariable('http_proxy', 'HTTP_PROXY'); + copyEnvVariable('https_proxy', 'HTTPS_PROXY'); + } + + private async selectPreset(): Promise { + const presetOptions: Preset[] = this.strykerPresets; + if (presetOptions.length) { + this.log.debug(`Found presets: ${JSON.stringify(presetOptions)}`); + return this.inquirer.promptPresets(presetOptions); + } else { + this.log.debug('No presets have been configured, reverting to custom configuration'); + return undefined; + } + } + + private async initiatePreset(configWriter: StrykerConfigWriter, selectedPreset: Preset) { + const presetConfig = await selectedPreset.createConfig(); + await configWriter.writePreset(presetConfig); + const selectedPackageManager = await this.selectPackageManager(); + this.installNpmDependencies(presetConfig.dependencies, selectedPackageManager); + } + + private async initiateCustom(configWriter: StrykerConfigWriter) { const selectedTestRunner = await this.selectTestRunner(); const selectedTestFramework = selectedTestRunner && !CommandTestRunner.is(selectedTestRunner.name) ? await this.selectTestFramework(selectedTestRunner) : null; @@ -47,22 +93,6 @@ export default class StrykerInitializer { selectedPackageManager, await this.fetchAdditionalConfig(npmDependencies)); this.installNpmDependencies(npmDependencies, selectedPackageManager); - this.out('Done configuring stryker. Please review `stryker.conf.js`, you might need to configure transpilers or your test runner correctly.'); - this.out('Let\'s kill some mutants with this command: `stryker run`'); - } - - /** - * The typed rest client works only with the specific HTTP_PROXY and HTTPS_PROXY env settings. - * Let's make sure they are available. - */ - private patchProxies() { - const copyEnvVariable = (from: string, to: string) => { - if (process.env[from] && !process.env[to]) { - process.env[to] = process.env[from]; - } - }; - copyEnvVariable('http_proxy', 'HTTP_PROXY'); - copyEnvVariable('https_proxy', 'HTTPS_PROXY'); } private async selectTestRunner(): Promise { diff --git a/packages/stryker/src/initializer/StrykerInquirer.ts b/packages/stryker/src/initializer/StrykerInquirer.ts index e2fe6d9e9d..ad926cc761 100644 --- a/packages/stryker/src/initializer/StrykerInquirer.ts +++ b/packages/stryker/src/initializer/StrykerInquirer.ts @@ -1,6 +1,7 @@ import * as inquirer from 'inquirer'; import PromptOption from './PromptOption'; import CommandTestRunner from '../test-runner/CommandTestRunner'; +import Preset from './presets/Preset'; export interface PromptResult { additionalNpmDependencies: string[]; @@ -9,6 +10,19 @@ export interface PromptResult { export class StrykerInquirer { + public async promptPresets(options: Preset[]): Promise { + const choices: inquirer.ChoiceType[] = options.map(_ => _.name); + choices.push(new inquirer.Separator()); + choices.push('None/other'); + const answers = await inquirer.prompt<{ preset: string }>({ + choices, + message: 'Are you using one of these frameworks? Then select a preset configuration.', + name: 'preset', + type: 'list' + }); + return options.find(_ => _.name === answers.preset); + } + public async promptTestRunners(options: PromptOption[]): Promise { const choices: inquirer.ChoiceType[] = options.map(_ => _.name); choices.push(new inquirer.Separator()); diff --git a/packages/stryker/src/initializer/StrykerPresets.ts b/packages/stryker/src/initializer/StrykerPresets.ts new file mode 100644 index 0000000000..f2424fe911 --- /dev/null +++ b/packages/stryker/src/initializer/StrykerPresets.ts @@ -0,0 +1,8 @@ +import { AngularPreset } from './presets/AngularPreset'; +import { ReactPreset } from './presets/ReactPreset'; +import Preset from './presets/Preset'; +import { VueJsPreset } from './presets/VueJsPreset'; + +// Add new presets here +const strykerPresets: Preset[] = [ new AngularPreset(), new ReactPreset(), new VueJsPreset() ]; +export default strykerPresets; diff --git a/packages/stryker/src/initializer/presets/AngularPreset.ts b/packages/stryker/src/initializer/presets/AngularPreset.ts new file mode 100644 index 0000000000..cfee88a288 --- /dev/null +++ b/packages/stryker/src/initializer/presets/AngularPreset.ts @@ -0,0 +1,39 @@ +import Preset from './Preset'; +import PresetConfiguration from './PresetConfiguration'; +import * as os from 'os'; + +const handbookUrl = 'https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/angular.md#angular'; + +export class AngularPreset implements Preset { + public readonly name = 'angular-cli'; + // Please keep config in sync with handbook + private readonly dependencies = [ + 'stryker', + 'stryker-karma-runner', + 'stryker-typescript', + 'stryker-html-reporter' + ]; + private readonly config = `{ + mutate: [ + 'src/**/*.ts', + '!src/**/*.spec.ts', + '!src/test.ts', + '!src/environments/*.ts' + ], + mutator: 'typescript', + testRunner: 'karma', + karma: { + configFile: 'src/karma.conf.js', + projectType: 'angular-cli', + config: { + browsers: ['ChromeHeadless'] + } + }, + reporters: ['progress', 'clear-text', 'html'], + maxConcurrentTestRunners: ${Math.floor(os.cpus().length / 2)}, // Recommended to use about half of your available cores when running stryker with angular. + coverageAnalysis: 'off' + }`; + public async createConfig(): Promise { + return { config: this.config, handbookUrl, dependencies: this.dependencies }; + } +} diff --git a/packages/stryker/src/initializer/presets/Preset.ts b/packages/stryker/src/initializer/presets/Preset.ts new file mode 100644 index 0000000000..3370f09861 --- /dev/null +++ b/packages/stryker/src/initializer/presets/Preset.ts @@ -0,0 +1,7 @@ +import PresetConfig from './PresetConfiguration'; + +interface Preset { + readonly name: string; + createConfig(): Promise; +} +export default Preset; diff --git a/packages/stryker/src/initializer/presets/PresetConfiguration.ts b/packages/stryker/src/initializer/presets/PresetConfiguration.ts new file mode 100644 index 0000000000..1c669f3284 --- /dev/null +++ b/packages/stryker/src/initializer/presets/PresetConfiguration.ts @@ -0,0 +1,5 @@ +export default interface PresetConfiguration { + config: string; + handbookUrl: string; + dependencies: string[]; +} diff --git a/packages/stryker/src/initializer/presets/ReactPreset.ts b/packages/stryker/src/initializer/presets/ReactPreset.ts new file mode 100644 index 0000000000..65269b8f80 --- /dev/null +++ b/packages/stryker/src/initializer/presets/ReactPreset.ts @@ -0,0 +1,60 @@ +import Preset from './Preset'; +import inquirer = require('inquirer'); +import PresetConfiguration from './PresetConfiguration'; + +const handbookUrl = 'https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/react.md#react'; + +/** + * More information can be found in the Stryker handbook: + * https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/react.md#react + */ +export class ReactPreset implements Preset { + public readonly name = 'react'; + private readonly generalDependencies = [ + 'stryker', + 'stryker-jest-runner', + 'stryker-html-reporter' + ]; + + private readonly sharedConfig = `testRunner: 'jest', + reporters: ['progress', 'clear-text', 'html'], + coverageAnalysis: 'off', + jest: { + projectType: 'react' + } + `; + + private readonly tsxDependencies = ['stryker-typescript', ...this.generalDependencies]; + private readonly tsxConf = `{ + mutate: ['src/**/*.ts?(x)', '!src/**/*@(.test|.spec|Spec).ts?(x)'], + mutator: 'typescript', + ${this.sharedConfig} + }`; + + private readonly jsxDependencies = ['stryker-javascript-mutator', ...this.generalDependencies]; + private readonly jsxConf = `{ + mutate: ['src/**/*.js?(x)', '!src/**/*@(.test|.spec|Spec).js?(x)'], + mutator: 'javascript', + ${this.sharedConfig} + }`; + + public async createConfig(): Promise { + const choices: inquirer.ChoiceType[] = ['JSX', 'TSX']; + const answers = await inquirer.prompt<{ choice: string }>({ + choices, + message: 'Is your project a JSX project or a TSX project?', + name: 'choice', + type: 'list' + }); + return this.load(answers.choice); + } + private load(choice: string): PresetConfiguration { + if (choice === 'JSX') { + return { config: this.jsxConf, handbookUrl, dependencies: this.jsxDependencies }; + } else if (choice === 'TSX') { + return { config: this.tsxConf, handbookUrl, dependencies: this.tsxDependencies }; + } else { + throw new Error(`Invalid project type ${choice}`); + } + } +} diff --git a/packages/stryker/src/initializer/presets/VueJsPreset.ts b/packages/stryker/src/initializer/presets/VueJsPreset.ts new file mode 100644 index 0000000000..33fc44bd98 --- /dev/null +++ b/packages/stryker/src/initializer/presets/VueJsPreset.ts @@ -0,0 +1,106 @@ +import Preset from './Preset'; +import inquirer = require('inquirer'); +import PresetConfiguration from './PresetConfiguration'; + +const handbookUrl = 'https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/vuejs.md#vuejs'; + +/** + * More information can be found in the Stryker handbook: + * https://github.com/stryker-mutator/stryker-handbook/blob/master/stryker/guides/vuejs.md#vuejs + */ +export class VueJsPreset implements Preset { + public readonly name = 'vueJs'; + private readonly generalDependencies = [ + 'stryker', + 'stryker-vue-mutator', + 'stryker-html-reporter' + ]; + + private readonly jestDependency = 'stryker-jest-runner'; + private readonly jestConf = `{ + mutate: ['src/**/*.js', 'src/**/*.ts', 'src/**/*.vue'], + mutator: 'vue', + testRunner: 'jest', + jest: { + // config: require('path/to/your/custom/jestConfig.js') + }, + reporter: ['progress', 'clear-text', 'html'], + coverageAnalysis: 'off' + }`; + + private readonly karmaDependency = 'stryker-karma-runner'; + private readonly karmaConf = `{ + mutate: ['src/**/*.js', 'src/**/*.ts', 'src/**/*.vue'], + mutator: 'vue', + testRunner: 'karma', + karma: { + configFile: 'test/unit/karma.conf.js', + config: { + browsers: ['ChromeHeadless'] + } + }, + reporter: ['progress', 'clear-text', 'html'], + coverageAnalysis: 'off' + }`; + + public async createConfig(): Promise { + const testRunnerChoices: inquirer.ChoiceType[] = ['karma', 'jest']; + const testRunnerAnswers = await inquirer.prompt<{ testRunner: string }>({ + choices: testRunnerChoices, + message: 'Which test runner do you want to use?', + name: 'testRunner', + type: 'list' + }); + const scriptChoices: inquirer.ChoiceType[] = ['typescript', 'javascript']; + const scriptAnswers = await inquirer.prompt<{ script: string }>({ + choices: scriptChoices, + message: 'Which language does your project use?', + name: 'script', + type: 'list' + }); + const chosenTestRunner = testRunnerAnswers.testRunner; + const chosenScript = scriptAnswers.script; + return { + config: this.getConfigString(chosenTestRunner), + dependencies: this.createDependencies(chosenTestRunner, chosenScript), + handbookUrl + }; + } + + private getConfigString(testRunner: string) { + if (testRunner === 'karma') { + return this.karmaConf; + } else if (testRunner === 'jest') { + return this.jestConf; + } else { + throw new Error(`Invalid test runner chosen: ${testRunner}`); + } + } + + private createDependencies(testRunner: string, script: string): string[] { + const dependencies = this.generalDependencies; + dependencies.push(this.getTestRunnerDependency(testRunner)); + dependencies.push(this.getScriptDependency(script)); + return dependencies; + } + + private getScriptDependency(script: string): string { + if (script === 'typescript') { + return 'stryker-typescript'; + } else if (script === 'javascript') { + return 'stryker-javascript-mutator'; + } else { + throw new Error(`Invalid script chosen: ${script}`); + } + } + + private getTestRunnerDependency(testRunner: string): string { + if (testRunner === 'karma') { + return this.karmaDependency; + } else if (testRunner === 'jest') { + return this.jestDependency; + } else { + throw new Error(`Invalid test runner chosen: ${testRunner}`); + } + } +} diff --git a/packages/stryker/stryker.conf.js b/packages/stryker/stryker.conf.js index 5f77b55ef6..b1826a2c29 100644 --- a/packages/stryker/stryker.conf.js +++ b/packages/stryker/stryker.conf.js @@ -1,64 +1,61 @@ -module.exports = function (config) { - - var typescript = true; - var es6 = true; - - if (typescript) { - config.set({ - files: [ - 'node_modules/stryker-api/*.@(js|map)', - 'node_modules/stryker-api/src/**/*.@(js|map)', - 'package.json', - 'src/**/*.ts', - '!src/**/*.d.ts', - 'test/helpers/**/*.ts', - 'test/unit/**/*.ts', - '!test/**/*.d.ts' - ], - symlinkNodeModules: false, - mutate: ['src/**/*.ts'], - coverageAnalysis: 'perTest', - tsconfigFile: 'tsconfig.json', - mutator: 'typescript', - transpilers: [ - 'typescript' - ], - mochaOptions: { - files: ['test/helpers/**/*.js', 'test/unit/**/*.js'] - } - }) - } else { - config.set({ - files: [ - 'test/helpers/**/*.js', - 'test/unit/**/*.js', - { pattern: 'src/**/*.js', included: false, mutated: true }, - { pattern: 'node_modules/stryker-api/*.js', included: false, mutated: false }, - { pattern: 'node_modules/stryker-api/src/**/*.js', included: false, mutated: false } - ], - coverageAnalysis: 'perTest', - mutator: es6 ? 'javascript' : 'es5' - }); - } - - config.set({ - testFramework: 'mocha', - testRunner: 'mocha', - reporters: ['progress', 'html', 'clear-text', 'event-recorder', 'dashboard'], - maxConcurrentTestRunners: 4, - thresholds: { - high: 80, - low: 60, - break: null - }, - fileLogLevel: 'trace', - logLevel: 'info', - plugins: [ - require.resolve('../stryker-mocha-runner/src/index'), - require.resolve('../stryker-mocha-framework/src/index'), - require.resolve('../stryker-html-reporter/src/index'), - require.resolve('../stryker-typescript/src/index'), - require.resolve('../stryker-javascript-mutator/src/index') - ] - }); -}; +module.exports = function (config) { + var typescript = true; + var es6 = true; + if (typescript) { + config.set({ + files: [ + 'node_modules/stryker-api/*.@(js|map)', + 'node_modules/stryker-api/src/**/*.@(js|map)', + 'package.json', + 'src/**/*.ts', + '!src/**/*.d.ts', + 'test/helpers/**/*.ts', + 'test/unit/**/*.ts', + '!test/**/*.d.ts' + ], + symlinkNodeModules: false, + mutate: ['src/**/*.ts'], + coverageAnalysis: 'perTest', + tsconfigFile: 'tsconfig.json', + mutator: 'typescript', + transpilers: [ + 'typescript' + ], + mochaOptions: { + files: ['test/helpers/**/*.js', 'test/unit/**/*.js'] + } + }) + } else { + config.set({ + files: [ + 'test/helpers/**/*.js', + 'test/unit/**/*.js', + { pattern: 'src/**/*.js', included: false, mutated: true }, + { pattern: 'node_modules/stryker-api/*.js', included: false, mutated: false }, + { pattern: 'node_modules/stryker-api/src/**/*.js', included: false, mutated: false } + ], + coverageAnalysis: 'perTest', + mutator: es6 ? 'javascript' : 'es5' + }); + } + config.set({ + testFramework: 'mocha', + testRunner: 'mocha', + reporters: ['progress', 'html', 'clear-text', 'event-recorder', 'dashboard'], + maxConcurrentTestRunners: 4, + thresholds: { + high: 80, + low: 60, + break: null + }, + fileLogLevel: 'trace', + logLevel: 'info', + plugins: [ + require.resolve('../stryker-mocha-runner/src/index'), + require.resolve('../stryker-mocha-framework/src/index'), + require.resolve('../stryker-html-reporter/src/index'), + require.resolve('../stryker-typescript/src/index'), + require.resolve('../stryker-javascript-mutator/src/index') + ] + }); + }; \ No newline at end of file diff --git a/packages/stryker/test/unit/initializer/PresetsSpec.ts b/packages/stryker/test/unit/initializer/PresetsSpec.ts new file mode 100644 index 0000000000..4ee1e838c8 --- /dev/null +++ b/packages/stryker/test/unit/initializer/PresetsSpec.ts @@ -0,0 +1,135 @@ +import { expect } from 'chai'; +import { AngularPreset } from '../../../src/initializer/presets/AngularPreset'; +import { ReactPreset } from '../../../src/initializer/presets/ReactPreset'; +import * as inquirer from 'inquirer'; +import { VueJsPreset } from '../../../src/initializer/presets/VueJsPreset'; + +describe('Presets', () => { + let inquirerPrompt: sinon.SinonStub; + + beforeEach(() => { + inquirerPrompt = sandbox.stub(inquirer, 'prompt'); + }); + describe('AngularPreset', () => { + let angularPreset: AngularPreset; + + beforeEach(() => { + angularPreset = new AngularPreset(); + }); + + it('should have the name "angular-cli"', () => { + expect(angularPreset.name).to.eq('angular-cli'); + }); + + it('should mutate typescript', async () => { + const config = await angularPreset.createConfig(); + expect(config.config).to.contain(`mutator: 'typescript'`); + }); + + it('should use the angular-cli', async () => { + const config = await angularPreset.createConfig(); + expect(config.config).to.contain(`projectType: 'angular-cli'`); + }); + }); + + describe('ReactPreset', () => { + let reactPreset: ReactPreset; + + beforeEach(() => { + reactPreset = new ReactPreset(); + }); + + it('should have the name "react"', () => { + expect(reactPreset.name).to.eq('react'); + }); + + it('should mutate typescript when TSX is chosen', async () => { + inquirerPrompt.resolves({ + choice: 'TSX' + }); + const config = await reactPreset.createConfig(); + expect(config.config).to.contain(`mutator: 'typescript'`); + }); + + it('should install stryker-typescript when TSX is chosen', async () => { + inquirerPrompt.resolves({ + choice: 'TSX' + }); + const config = await reactPreset.createConfig(); + expect(config.dependencies).to.include('stryker-typescript'); + }); + + it('should mutate javascript when JSX is chosen', async () => { + inquirerPrompt.resolves({ + choice: 'JSX' + }); + const config = await reactPreset.createConfig(); + expect(config.config).to.include(`mutator: 'javascript'`); + }); + + it('should install stryker-javascript-mutator when JSX is chosen', async () => { + inquirerPrompt.resolves({ + choice: 'JSX' + }); + const config = await reactPreset.createConfig(); + expect(config.dependencies).to.include('stryker-javascript-mutator'); + }); + }); + + describe('VueJsPreset', () => { + let vueJsPreset: VueJsPreset; + + beforeEach(() => { + vueJsPreset = new VueJsPreset(); + inquirerPrompt.resolves({ + script: 'typescript', + testRunner: 'karma' + }); + }); + + it('should have the name "vueJs"', () => { + expect(vueJsPreset.name).to.eq('vueJs'); + }); + + it('should use the vue mutator', async () => { + const config = await vueJsPreset.createConfig(); + expect(config.config).to.contain(`mutator: 'vue'`); + }); + + it('should install stryker-karma-runner when karma is chosen', async () => { + inquirerPrompt.resolves({ + script: 'typescript', + testRunner: 'karma' + }); + const config = await vueJsPreset.createConfig(); + expect(config.dependencies).to.include('stryker-karma-runner'); + }); + + it('should install stryker-jest-runner when jest is chosen', async () => { + inquirerPrompt.resolves({ + script: 'typescript', + testRunner: 'jest' + }); + const config = await vueJsPreset.createConfig(); + expect(config.dependencies).to.include('stryker-jest-runner'); + }); + + it('should install stryker-typescript when typescript is chosen', async () => { + inquirerPrompt.resolves({ + script: 'typescript', + testRunner: 'karma' + }); + const config = await vueJsPreset.createConfig(); + expect(config.dependencies).to.include('stryker-typescript'); + }); + + it('should install stryker-javascript-mutator when javascript is chosen', async () => { + inquirerPrompt.resolves({ + script: 'javascript', + testRunner: 'karma' + }); + const config = await vueJsPreset.createConfig(); + expect(config.dependencies).to.include('stryker-javascript-mutator'); + }); + }); +}); diff --git a/packages/stryker/test/unit/initializer/StrykerInitializerSpec.ts b/packages/stryker/test/unit/initializer/StrykerInitializerSpec.ts index cc2c0581e2..bcad7fd558 100644 --- a/packages/stryker/test/unit/initializer/StrykerInitializerSpec.ts +++ b/packages/stryker/test/unit/initializer/StrykerInitializerSpec.ts @@ -8,6 +8,10 @@ import StrykerInitializer from '../../../src/initializer/StrykerInitializer'; import * as restClient from 'typed-rest-client/RestClient'; import currentLogMock from '../../helpers/logMock'; import { Mock } from '../../helpers/producers'; +import NpmClient from '../../../src/initializer/NpmClient'; +import { format } from 'prettier'; +import PresetConfiguration from '../../../src/initializer/presets/PresetConfiguration'; +import Preset from '../../../src/initializer/presets/Preset'; describe('StrykerInitializer', () => { let log: Mock; @@ -19,10 +23,17 @@ describe('StrykerInitializer', () => { let restClientPackageGet: sinon.SinonStub; let restClientSearchGet: sinon.SinonStub; let out: sinon.SinonStub; + let presets: Preset[]; + let presetMock: Mock; beforeEach(() => { log = currentLogMock(); out = sandbox.stub(); + presets = []; + presetMock = { + createConfig: sandbox.stub(), + name: 'awesome-preset' + }; inquirerPrompt = sandbox.stub(inquirer, 'prompt'); childExecSync = sandbox.stub(child, 'execSync'); fsWriteFile = sandbox.stub(fsAsPromised, 'writeFile'); @@ -36,7 +47,7 @@ describe('StrykerInitializer', () => { .withArgs('npm').returns({ get: restClientPackageGet }); - sut = new StrykerInitializer(out); + sut = new StrykerInitializer(out, new NpmClient(), presets); }); afterEach(() => { @@ -67,9 +78,10 @@ describe('StrykerInitializer', () => { 'stryker-webpack': null }); fsWriteFile.resolves({}); + presets.push(presetMock); }); - it('should prompt for test runner, test framework, mutator, transpilers, reporters, and package manager', async () => { + it('should prompt for preset, test runner, test framework, mutator, transpilers, reporters, and package manager', async () => { arrangeAnswers({ mutator: 'typescript', packageManager: 'yarn', @@ -79,15 +91,19 @@ describe('StrykerInitializer', () => { transpilers: ['webpack'] }); await sut.initialize(); - expect(inquirerPrompt).to.have.been.callCount(6); - const [promptTestRunner, promptTestFramework, promptMutator, promptTranspilers, promptReporters, promptPackageManagers]: inquirer.Question[] = [ + expect(inquirerPrompt).to.have.been.callCount(7); + const [promptPreset, promptTestRunner, promptTestFramework, promptMutator, promptTranspilers, promptReporters, promptPackageManagers]: inquirer.Question[] = [ inquirerPrompt.getCall(0).args[0], inquirerPrompt.getCall(1).args[0], inquirerPrompt.getCall(2).args[0], inquirerPrompt.getCall(3).args[0], inquirerPrompt.getCall(4).args[0], - inquirerPrompt.getCall(5).args[0] + inquirerPrompt.getCall(5).args[0], + inquirerPrompt.getCall(6).args[0], ]; + expect(promptPreset.type).to.eq('list'); + expect(promptPreset.name).to.eq('preset'); + expect(promptPreset.choices).to.deep.eq(['awesome-preset', new inquirer.Separator(), 'None/other']); expect(promptTestRunner.type).to.eq('list'); expect(promptTestRunner.name).to.eq('testRunner'); expect(promptTestRunner.choices).to.deep.eq(['awesome', 'hyper', 'ghost', new inquirer.Separator(), 'command']); @@ -103,6 +119,73 @@ describe('StrykerInitializer', () => { expect(promptPackageManagers.choices).to.deep.eq(['npm', 'yarn']); }); + it('should immediately complete when a preset and package manager is chosen', async () => { + inquirerPrompt.resolves({ + packageManager: 'npm', + preset: 'awesome-preset' + }); + resolvePresetConfig(); + await sut.initialize(); + expect(inquirerPrompt).to.have.been.callCount(2); + expect(out).to.have.been.calledWith('Done configuring stryker. Please review `stryker.conf.js`, you might need to configure transpilers or your test runner correctly.'); + expect(out).to.have.been.calledWith('Let\'s kill some mutants with this command: `stryker run`'); + }); + + it('should correctly load the stryker configuration file', async () => { + const config = `{ + 'awesome-conf': 'awesome', + }`; + const handbookUrl = 'https://awesome-preset.org'; + resolvePresetConfig({ + config, + handbookUrl + }); + const expectedOutput = format(` + // This config was generated using a preset. + // Please see the handbook for more information: ${handbookUrl} + module.exports = function(config){ + config.set( + ${config} + ); + }`, { parser: 'babylon' }); + inquirerPrompt.resolves({ + packageManager: 'npm', + preset: 'awesome-preset' + }); + await sut.initialize(); + expect(fsWriteFile).to.have.been.calledWith('stryker.conf.js', expectedOutput); + }); + + it('should correctly load dependencies from the preset', async () => { + resolvePresetConfig({ dependencies: ['my-awesome-dependency', 'another-awesome-dependency'] }); + inquirerPrompt.resolves({ + packageManager: 'npm', + preset: 'awesome-preset' + }); + await sut.initialize(); + expect(fsWriteFile).to.have.been.calledOnce; + expect(childExecSync).to.have.been.calledWith('npm i --save-dev stryker-api my-awesome-dependency another-awesome-dependency', { stdio: [0, 1, 2] }); + }); + + it('should correctly load configuration from a preset', async () => { + resolvePresetConfig(); + inquirerPrompt.resolves({ + packageManager: 'npm', + preset: 'awesome-preset' + }); + await sut.initialize(); + expect(inquirerPrompt).to.have.been.callCount(2); + const [promptPreset, promptPackageManager]: inquirer.Question[] = [ + inquirerPrompt.getCall(0).args[0], + inquirerPrompt.getCall(1).args[0] + ]; + expect(promptPreset.type).to.eq('list'); + expect(promptPreset.name).to.eq('preset'); + expect(promptPreset.choices).to.deep.eq(['awesome-preset', new inquirer.Separator(), 'None/other']); + expect(promptPackageManager.type).to.eq('list'); + expect(promptPackageManager.choices).to.deep.eq(['npm', 'yarn']); + }); + it('should not prompt for testFramework if test runner is "command"', async () => { arrangeAnswers({ testRunner: 'command' }); await sut.initialize(); @@ -118,7 +201,7 @@ describe('StrykerInitializer', () => { transpilers: ['webpack'] }); await sut.initialize(); - expect(inquirerPrompt).to.have.been.callCount(6); + expect(inquirerPrompt).to.have.been.callCount(7); expect(out).to.have.been.calledWith('OK, downgrading coverageAnalysis to "all"'); expect(fsAsPromised.writeFile).to.have.been.calledWith('stryker.conf.js', sinon.match('coverageAnalysis: "all"')); }); @@ -182,7 +265,7 @@ describe('StrykerInitializer', () => { it('should not prompt for test framework', async () => { await sut.initialize(); - expect(inquirerPrompt).to.have.been.callCount(5); + expect(inquirerPrompt).to.have.been.callCount(6); expect(inquirerPrompt).not.calledWithMatch(sinon.match({ name: 'testFramework' })); }); @@ -418,6 +501,7 @@ describe('StrykerInitializer', () => { }; interface StrykerInitAnswers { + preset: string | null; testFramework: string; testRunner: string; mutator: string; @@ -430,6 +514,7 @@ describe('StrykerInitializer', () => { const answers: StrykerInitAnswers = Object.assign({ mutator: 'typescript', packageManager: 'yarn', + preset: null, reporters: ['dimension', 'mars'], testFramework: 'awesome', testRunner: 'awesome', @@ -437,4 +522,13 @@ describe('StrykerInitializer', () => { }, answerOverrides); inquirerPrompt.resolves(answers); } + + function resolvePresetConfig(presetConfigOverrides?: Partial) { + const presetConfig: PresetConfiguration = { + config: '', + dependencies: [], + handbookUrl: '' + }; + presetMock.createConfig.resolves(Object.assign({}, presetConfig, presetConfigOverrides)); + } });