diff --git a/CHANGELOG.md b/CHANGELOG.md index 36003f385552..fa9426ef5523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - `[jest-runner]` print stack trace when `process.exit` is called from user code ([#6714](https://github.com/facebook/jest/pull/6714)) - `[jest-each]` introduces `%#` option to add index of the test to its title ([#6414](https://github.com/facebook/jest/pull/6414)) - `[pretty-format]` Support serializing `DocumentFragment` ([#6705](https://github.com/facebook/jest/pull/6705)) +- `[jest-validate]` Add `recursive` and `recursiveBlacklist` options for deep config checks ([#6802](https://github.com/facebook/jest/pull/6802)) ### Fixes diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 5581f68247a7..193ae99a20f6 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -335,6 +335,15 @@ export default function normalize(options: InitialOptions, argv: Argv) { comment: DOCUMENTATION_NOTE, deprecatedConfig: DEPRECATED_CONFIG, exampleConfig: VALID_CONFIG, + recursiveBlacklist: [ + 'collectCoverageOnlyFrom', + // 'coverageThreshold' allows to use 'global' and glob strings on the same + // level, there's currently no way we can deal with such config + 'coverageThreshold', + 'globals', + 'moduleNameMapper', + 'transform', + ], }); options = normalizePreprocessor( diff --git a/packages/jest-config/src/valid_config.js b/packages/jest-config/src/valid_config.js index 9dd1f21abb95..d711a520a7fc 100644 --- a/packages/jest-config/src/valid_config.js +++ b/packages/jest-config/src/valid_config.js @@ -21,7 +21,7 @@ export default ({ cache: true, cacheDirectory: '/tmp/user/jest', changedFilesWithAncestor: false, - changedSince: '', + changedSince: 'master', clearMocks: false, collectCoverage: true, collectCoverageFrom: ['src', '!public'], @@ -34,6 +34,9 @@ export default ({ coverageThreshold: { global: { branches: 50, + functions: 100, + lines: 100, + statements: 100, }, }, displayName: 'project-name', @@ -44,8 +47,11 @@ export default ({ forceExit: false, globalSetup: 'setup.js', globalTeardown: 'teardown.js', - globals: {}, + globals: {__DEV__: true}, haste: { + defaultPlatform: 'ios', + hasteImplModulePath: '/haste_impl.js', + platforms: ['ios', 'android'], providesModuleNodeModules: ['react', 'react-native'], }, json: false, @@ -87,7 +93,7 @@ export default ({ skipNodeResolution: false, snapshotSerializers: ['my-serializer-module'], testEnvironment: 'jest-environment-jsdom', - testEnvironmentOptions: {}, + testEnvironmentOptions: {userAgent: 'Agent/007'}, testFailureExitCode: 1, testLocationInResults: false, testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'], @@ -107,7 +113,16 @@ export default ({ useStderr: false, verbose: false, watch: false, - watchPathIgnorePatterns: [], - watchPlugins: [], + watchPathIgnorePatterns: ['/e2e/'], + watchPlugins: [ + 'path/to/yourWatchPlugin', + [ + 'jest-watch-typeahead/filename', + { + key: 'k', + prompt: 'do something with my custom prompt', + }, + ], + ], watchman: true, }: InitialOptions); diff --git a/packages/jest-validate/README.md b/packages/jest-validate/README.md index a490975fe96f..178bb7c486da 100644 --- a/packages/jest-validate/README.md +++ b/packages/jest-validate/README.md @@ -18,6 +18,7 @@ Where `ValidationOptions` are: ```js type ValidationOptions = { + blacklist?: Array, comment?: string, condition?: (option: any, validOption: any) => boolean, deprecate?: ( @@ -34,6 +35,7 @@ type ValidationOptions = { options: ValidationOptions, ) => void, exampleConfig: Object, + recursive?: boolean, title?: Title, unknown?: ( config: Object, @@ -60,11 +62,13 @@ Almost anything can be overwritten to suite your needs. ### Options +- `recursiveBlacklist` – optional array of string keyPaths that should be excluded from deep (recursive) validation. - `comment` – optional string to be rendered below error/warning message. - `condition` – an optional function with validation condition. - `deprecate`, `error`, `unknown` – optional functions responsible for displaying warning and error messages. - `deprecatedConfig` – optional object with deprecated config keys. - `exampleConfig` – the only **required** option with configuration against which you'd like to test. +- `recursive` - optional boolean determining whether recursively compare `exampleConfig` to `config` (default: `true`). - `title` – optional object of titles for errors and messages. You will find examples of `condition`, `deprecate`, `error`, `unknown`, and `deprecatedConfig` inside source of this repository, named respectively. @@ -116,7 +120,9 @@ This will output: Example: { - "transform": {"^.+\\.js$": "/preprocessor.js"} + "transform": { + "^.+\\.js$": "/preprocessor.js" + } } Documentation: http://custom-docs.com diff --git a/packages/jest-validate/src/__tests__/__snapshots__/validate.test.js.snap b/packages/jest-validate/src/__tests__/__snapshots__/validate.test.js.snap index de79961455f5..2571b7454576 100644 --- a/packages/jest-validate/src/__tests__/__snapshots__/validate.test.js.snap +++ b/packages/jest-validate/src/__tests__/__snapshots__/validate.test.js.snap @@ -32,7 +32,12 @@ exports[`pretty prints valid config for Array 1`] = ` Example: { - \\"coverageReporters\\": [\\"json\\", \\"text\\", \\"lcov\\", \\"clover\\"] + \\"coverageReporters\\": [ + \\"json\\", + \\"text\\", + \\"lcov\\", + \\"clover\\" + ] } " `; @@ -77,7 +82,12 @@ exports[`pretty prints valid config for Object 1`] = ` Example: { - \\"haste\\": {\\"providesModuleNodeModules\\": [\\"react\\", \\"react-native\\"]} + \\"haste\\": { + \\"providesModuleNodeModules\\": [ + \\"react\\", + \\"react-native\\" + ] + } } " `; @@ -122,7 +132,10 @@ exports[`works with custom errors 1`] = ` Example: { - \\"test\\": [1, 2] + \\"test\\": [ + 1, + 2 + ] } My custom comment" diff --git a/packages/jest-validate/src/__tests__/validate.test.js b/packages/jest-validate/src/__tests__/validate.test.js index abef68fbd9f3..ec4de4d38b92 100644 --- a/packages/jest-validate/src/__tests__/validate.test.js +++ b/packages/jest-validate/src/__tests__/validate.test.js @@ -18,7 +18,7 @@ const { deprecatedConfig, } = require('./fixtures/jest_config'); -test('validates default Jest config', () => { +test('recursively validates default Jest config', () => { expect( validate(defaultConfig, { exampleConfig: validConfig, @@ -29,7 +29,7 @@ test('validates default Jest config', () => { }); }); -test('validates default jest-validate config', () => { +test('recursively validates default jest-validate config', () => { expect( validate(jestValidateDefaultConfig, { exampleConfig: jestValidateExampleConfig, @@ -40,19 +40,17 @@ test('validates default jest-validate config', () => { }); }); -[ - [{automock: []}, 'Boolean'], - [{coverageReporters: {}}, 'Array'], - [{preset: 1337}, 'String'], - [{haste: 42}, 'Object'], -].forEach(([config, type]) => { - test(`pretty prints valid config for ${type}`, () => { - expect(() => - validate(config, { - exampleConfig: validConfig, - }), - ).toThrowErrorMatchingSnapshot(); - }); +test.each([ + ['Boolean', {automock: []}], + ['Array', {coverageReporters: {}}], + ['String', {preset: 1337}], + ['Object', {haste: 42}], +])('pretty prints valid config for %s', (type, config) => { + expect(() => + validate(config, { + exampleConfig: validConfig, + }), + ).toThrowErrorMatchingSnapshot(); }); test(`pretty prints valid config for Function`, () => { @@ -76,6 +74,54 @@ test('omits null and undefined config values', () => { }); }); +test('recursively omits null and undefined config values', () => { + const config = { + haste: { + providesModuleNodeModules: null, + }, + }; + expect( + validate(config, {exampleConfig: validConfig, recursive: true}), + ).toEqual({ + hasDeprecationWarnings: false, + isValid: true, + }); +}); + +test('respects blacklist', () => { + const warn = console.warn; + console.warn = jest.fn(); + const config = { + something: { + nested: { + some_random_key: 'value', + some_random_key2: 'value2', + }, + }, + }; + const exampleConfig = { + something: { + nested: { + test: true, + }, + }, + }; + + validate(config, {exampleConfig}); + + expect(console.warn).toBeCalled(); + + console.warn.mockReset(); + + validate(config, { + exampleConfig, + recursiveBlacklist: ['something.nested'], + }); + + expect(console.warn).not.toBeCalled(); + console.warn = warn; +}); + test('displays warning for unknown config options', () => { const config = {unkwon: {}}; const validConfig = {unknown: 'string'}; diff --git a/packages/jest-validate/src/default_config.js b/packages/jest-validate/src/default_config.js index a0d85b428146..735037968c60 100644 --- a/packages/jest-validate/src/default_config.js +++ b/packages/jest-validate/src/default_config.js @@ -12,7 +12,6 @@ import type {ValidationOptions} from './types'; import {deprecationWarning} from './deprecated'; import {unknownOptionWarning} from './warnings'; import {errorMessage} from './errors'; -import exampleConfig from './example_config'; import validationCondition from './condition'; import {ERROR, DEPRECATION, WARNING} from './utils'; @@ -22,7 +21,9 @@ export default ({ deprecate: deprecationWarning, deprecatedConfig: {}, error: errorMessage, - exampleConfig, + exampleConfig: {}, + recursive: true, + recursiveBlacklist: [], title: { deprecation: DEPRECATION, error: ERROR, diff --git a/packages/jest-validate/src/errors.js b/packages/jest-validate/src/errors.js index 00d6fae5e47b..cb1589c71616 100644 --- a/packages/jest-validate/src/errors.js +++ b/packages/jest-validate/src/errors.js @@ -11,22 +11,27 @@ import type {ValidationOptions} from './types'; import chalk from 'chalk'; import getType from 'jest-get-type'; -import {format, ValidationError, ERROR} from './utils'; +import {formatPrettyObject, ValidationError, ERROR} from './utils'; export const errorMessage = ( option: string, received: any, defaultValue: any, options: ValidationOptions, + path?: Array, ): void => { - const message = ` Option ${chalk.bold(`"${option}"`)} must be of type: + const message = ` Option ${chalk.bold( + `"${path && path.length > 0 ? path.join('.') + '.' : ''}${option}"`, + )} must be of type: ${chalk.bold.green(getType(defaultValue))} but instead received: ${chalk.bold.red(getType(received))} Example: { - ${chalk.bold(`"${option}"`)}: ${chalk.bold(format(defaultValue))} + ${chalk.bold(`"${option}"`)}: ${chalk.bold( + formatPrettyObject(defaultValue), + )} }`; const comment = options.comment; diff --git a/packages/jest-validate/src/example_config.js b/packages/jest-validate/src/example_config.js index 4503dda59868..8230a568e3ed 100644 --- a/packages/jest-validate/src/example_config.js +++ b/packages/jest-validate/src/example_config.js @@ -18,6 +18,8 @@ const config: ValidationOptions = { }, error: (option, received, defaultValue, options) => {}, exampleConfig: {key: 'value', test: 'case'}, + recursive: true, + recursiveBlacklist: [], title: { deprecation: 'Deprecation Warning', error: 'Validation Error', diff --git a/packages/jest-validate/src/types.js b/packages/jest-validate/src/types.js index 3e32e567be24..89fb390942b4 100644 --- a/packages/jest-validate/src/types.js +++ b/packages/jest-validate/src/types.js @@ -28,13 +28,17 @@ export type ValidationOptions = { received: any, defaultValue: any, options: ValidationOptions, + path?: Array, ) => void, exampleConfig: Object, + recursive?: boolean, + recursiveBlacklist?: Array, title?: Title, unknown?: ( config: Object, exampleConfig: Object, option: string, options: ValidationOptions, + path?: Array, ) => void, }; diff --git a/packages/jest-validate/src/utils.js b/packages/jest-validate/src/utils.js index dccc27def3fd..f44912b6b723 100644 --- a/packages/jest-validate/src/utils.js +++ b/packages/jest-validate/src/utils.js @@ -21,6 +21,13 @@ export const format = (value: any): string => ? value.toString() : prettyFormat(value, {min: true}); +export const formatPrettyObject = (value: any): string => + typeof value === 'function' + ? value.toString() + : JSON.stringify(value, null, 2) + .split('\n') + .join('\n '); + export class ValidationError extends Error { name: string; message: string; diff --git a/packages/jest-validate/src/validate.js b/packages/jest-validate/src/validate.js index 4faf817b67a4..96c54cdcf7a4 100644 --- a/packages/jest-validate/src/validate.js +++ b/packages/jest-validate/src/validate.js @@ -11,8 +11,28 @@ import type {ValidationOptions} from './types'; import defaultConfig from './default_config'; -const _validate = (config: Object, options: ValidationOptions) => { - let hasDeprecationWarnings = false; +let hasDeprecationWarnings = false; + +const shouldSkipValidationForPath = ( + path: Array, + key: string, + blacklist: ?Array, +) => (blacklist ? blacklist.includes([...path, key].join('.')) : false); + +const _validate = ( + config: Object, + exampleConfig: Object, + options: ValidationOptions, + path: Array = [], +) => { + if ( + typeof config !== 'object' || + config == null || + typeof exampleConfig !== 'object' || + exampleConfig == null + ) { + return {hasDeprecationWarnings}; + } for (const key in config) { if ( @@ -28,17 +48,30 @@ const _validate = (config: Object, options: ValidationOptions) => { ); hasDeprecationWarnings = hasDeprecationWarnings || isDeprecatedKey; - } else if (hasOwnProperty.call(options.exampleConfig, key)) { + } else if (hasOwnProperty.call(exampleConfig, key)) { if ( typeof options.condition === 'function' && typeof options.error === 'function' && - !options.condition(config[key], options.exampleConfig[key]) + !options.condition(config[key], exampleConfig[key]) ) { - options.error(key, config[key], options.exampleConfig[key], options); + options.error(key, config[key], exampleConfig[key], options, path); } + } else if ( + shouldSkipValidationForPath(path, key, options.recursiveBlacklist) + ) { + // skip validating unknown options inside blacklisted paths } else { options.unknown && - options.unknown(config, options.exampleConfig, key, options); + options.unknown(config, exampleConfig, key, options, path); + } + + if ( + options.recursive && + !Array.isArray(exampleConfig[key]) && + options.recursiveBlacklist && + !shouldSkipValidationForPath(path, key, options.recursiveBlacklist) + ) { + _validate(config[key], exampleConfig[key], options, [...path, key]); } } @@ -46,7 +79,7 @@ const _validate = (config: Object, options: ValidationOptions) => { }; const validate = (config: Object, options: ValidationOptions) => { - _validate(options, defaultConfig); // validate against jest-validate config + hasDeprecationWarnings = false; const defaultedOptions: ValidationOptions = Object.assign( {}, @@ -55,10 +88,14 @@ const validate = (config: Object, options: ValidationOptions) => { {title: Object.assign({}, defaultConfig.title, options.title)}, ); - const {hasDeprecationWarnings} = _validate(config, defaultedOptions); + const {hasDeprecationWarnings: hdw} = _validate( + config, + options.exampleConfig, + defaultedOptions, + ); return { - hasDeprecationWarnings, + hasDeprecationWarnings: hdw, isValid: true, }; }; diff --git a/packages/jest-validate/src/warnings.js b/packages/jest-validate/src/warnings.js index 435576c04eb6..69147f44718b 100644 --- a/packages/jest-validate/src/warnings.js +++ b/packages/jest-validate/src/warnings.js @@ -22,15 +22,16 @@ export const unknownOptionWarning = ( exampleConfig: Object, option: string, options: ValidationOptions, + path?: Array, ): void => { const didYouMean = createDidYouMeanMessage( option, Object.keys(exampleConfig), ); const message = - ` Unknown option ${chalk.bold(`"${option}"`)} with value ${chalk.bold( - format(config[option]), - )} was found.` + + ` Unknown option ${chalk.bold( + `"${path && path.length > 0 ? path.join('.') + '.' : ''}${option}"`, + )} with value ${chalk.bold(format(config[option]))} was found.` + (didYouMean && ` ${didYouMean}`) + `\n This is probably a typing mistake. Fixing it will remove this message.`;