diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f899df9a6f7..eb0172df5295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - `[jest-runtime]` If `require` fails without a file extension, print all files that match with one ([#7160](https://github.com/facebook/jest/pull/7160)) - `[jest-haste-map]` Make `ignorePattern` optional ([#7166](https://github.com/facebook/jest/pull/7166)) - `[jest-runtime]` Remove `cacheDirectory` from `ignorePattern` for `HasteMap` if not necessary ([#7166](https://github.com/facebook/jest/pull/7166)) +- `[jest-validate]` Add syntax to validate multiple permitted types ### Fixes diff --git a/packages/jest-validate/README.md b/packages/jest-validate/README.md index 178bb7c486da..cd3122967061 100644 --- a/packages/jest-validate/README.md +++ b/packages/jest-validate/README.md @@ -73,6 +73,16 @@ Almost anything can be overwritten to suite your needs. You will find examples of `condition`, `deprecate`, `error`, `unknown`, and `deprecatedConfig` inside source of this repository, named respectively. +## exampleConfig syntax + +`exampleConfig` should be an object with key/value pairs that contain an example of a valid value for each key. A configuration value is considered valid when: + +- it matches the JavaScript type of the example value, e.g. `string`, `number`, `array`, `boolean`, `function`, or `object` +- it is `null` or `undefined` +- the example value is an array where the first element is the special value `MultipleValidOptions`, and the value matches the type of _any_ of the other values in the array + +The last condition is a special syntax that allows validating where more than one type is permissible; see example below. It's acceptable to have multiple values of the same type in the example, so you can also use this syntax to provide more than one example. When a validation failure occurs, the error message will show all other values in the array as examples. + ## Examples Minimal example: @@ -128,6 +138,41 @@ This will output: Documentation: http://custom-docs.com ``` +## Example validating multiple types + +```js +import {MultipleValidOptions} from 'jest-validate'; + +validate(config, { + // `bar` will accept either a string or a number + bar: [MultipleValidOptions, 'string is ok', 2], +}); +``` + +#### Error: + +```bash +● Validation Error: + + Option foo must be of type: + string or number + but instead received: + array + + Example: + { + "bar": "string is ok" + } + + or + + { + "bar": 2 + } + + Documentation: http://custom-docs.com +``` + #### Deprecation Based on `deprecatedConfig` object with proper deprecation messages. Note custom title: 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 2571b7454576..a08516dd6839 100644 --- a/packages/jest-validate/src/__tests__/__snapshots__/validate.test.js.snap +++ b/packages/jest-validate/src/__tests__/__snapshots__/validate.test.js.snap @@ -1,5 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Repeated types within multiple valid examples are coalesced in error report 1`] = ` +" Validation Error: + + Option \\"foo\\" must be of type: + string or number + but instead received: + boolean + + Example: + { + \\"foo\\": \\"foo\\" + } + + or + + { + \\"foo\\": \\"bar\\" + } + + or + + { + \\"foo\\": 2 + } +" +`; + exports[`displays warning for deprecated config options 1`] = ` " Deprecation Warning: @@ -107,6 +134,29 @@ exports[`pretty prints valid config for String 1`] = ` " `; +exports[`reports errors nicely when failing with multiple valid options 1`] = ` +" Validation Error: + + Option \\"foo\\" must be of type: + string or array + but instead received: + number + + Example: + { + \\"foo\\": \\"text\\" + } + + or + + { + \\"foo\\": [ + \\"text\\" + ] + } +" +`; + exports[`works with custom deprecations 1`] = ` "My Custom Deprecation Warning: diff --git a/packages/jest-validate/src/__tests__/validate.test.js b/packages/jest-validate/src/__tests__/validate.test.js index ec4de4d38b92..7ea5c2eafc77 100644 --- a/packages/jest-validate/src/__tests__/validate.test.js +++ b/packages/jest-validate/src/__tests__/validate.test.js @@ -9,6 +9,7 @@ 'use strict'; import validate from '../validate'; +import {MultipleValidOptions} from '../condition'; import jestValidateExampleConfig from '../example_config'; import jestValidateDefaultConfig from '../default_config'; @@ -206,3 +207,62 @@ test('works with custom deprecations', () => { expect(console.warn.mock.calls[0][0]).toMatchSnapshot(); console.warn = warn; }); + +test('works with multiple valid types', () => { + const exampleConfig = { + foo: [MultipleValidOptions, 'text', ['text']], + }; + + expect( + validate( + {foo: 'foo'}, + { + exampleConfig, + }, + ), + ).toEqual({ + hasDeprecationWarnings: false, + isValid: true, + }); + expect( + validate( + {foo: ['foo']}, + { + exampleConfig, + }, + ), + ).toEqual({ + hasDeprecationWarnings: false, + isValid: true, + }); +}); + +test('reports errors nicely when failing with multiple valid options', () => { + const exampleConfig = { + foo: [MultipleValidOptions, 'text', ['text']], + }; + + expect(() => + validate( + {foo: 2}, + { + exampleConfig, + }, + ), + ).toThrowErrorMatchingSnapshot(); +}); + +test('Repeated types within multiple valid examples are coalesced in error report', () => { + const exampleConfig = { + foo: [MultipleValidOptions, 'foo', 'bar', 2], + }; + + expect(() => + validate( + {foo: false}, + { + exampleConfig, + }, + ), + ).toThrowErrorMatchingSnapshot(); +}); diff --git a/packages/jest-validate/src/condition.js b/packages/jest-validate/src/condition.js index 20d1ed545fde..e62d18c7ca66 100644 --- a/packages/jest-validate/src/condition.js +++ b/packages/jest-validate/src/condition.js @@ -9,13 +9,29 @@ const toString = Object.prototype.toString; -export default function validationCondition( - option: any, - validOption: any, -): boolean { +export const MultipleValidOptions = Symbol('JEST_MULTIPLE_VALID_OPTIONS'); + +function validationConditionSingle(option: any, validOption: any): boolean { return ( option === null || option === undefined || toString.call(option) === toString.call(validOption) ); } + +export function getConditions(validOption: any) { + if ( + Array.isArray(validOption) && + validOption.length && + validOption[0] === MultipleValidOptions + ) { + return validOption.slice(1); + } + return [validOption]; +} + +export function validationCondition(option: any, validOption: any): boolean { + return getConditions(validOption).some(e => + validationConditionSingle(option, e), + ); +} diff --git a/packages/jest-validate/src/default_config.js b/packages/jest-validate/src/default_config.js index 735037968c60..2d3d097e6c67 100644 --- a/packages/jest-validate/src/default_config.js +++ b/packages/jest-validate/src/default_config.js @@ -12,7 +12,7 @@ import type {ValidationOptions} from './types'; import {deprecationWarning} from './deprecated'; import {unknownOptionWarning} from './warnings'; import {errorMessage} from './errors'; -import validationCondition from './condition'; +import {validationCondition} from './condition'; import {ERROR, DEPRECATION, WARNING} from './utils'; export default ({ diff --git a/packages/jest-validate/src/errors.js b/packages/jest-validate/src/errors.js index cb1589c71616..b4dfa6e06c42 100644 --- a/packages/jest-validate/src/errors.js +++ b/packages/jest-validate/src/errors.js @@ -12,6 +12,7 @@ import type {ValidationOptions} from './types'; import chalk from 'chalk'; import getType from 'jest-get-type'; import {formatPrettyObject, ValidationError, ERROR} from './utils'; +import {getConditions} from './condition'; export const errorMessage = ( option: string, @@ -20,22 +21,46 @@ export const errorMessage = ( options: ValidationOptions, path?: Array, ): void => { + const conditions = getConditions(defaultValue); + const validTypes = conditions + .map(getType) + .filter(uniqueFilter()) + .join(' or '); + const message = ` Option ${chalk.bold( `"${path && path.length > 0 ? path.join('.') + '.' : ''}${option}"`, )} must be of type: - ${chalk.bold.green(getType(defaultValue))} + ${chalk.bold.green(validTypes)} but instead received: ${chalk.bold.red(getType(received))} Example: - { - ${chalk.bold(`"${option}"`)}: ${chalk.bold( - formatPrettyObject(defaultValue), - )} - }`; +${formatExamples(option, conditions)}`; const comment = options.comment; const name = (options.title && options.title.error) || ERROR; throw new ValidationError(name, message, comment); }; + +function formatExamples(option: string, examples: Array) { + return examples.map( + e => ` { + ${chalk.bold(`"${option}"`)}: ${chalk.bold(formatPrettyObject(e))} + }`, + ).join(` + + or + +`); +} + +function uniqueFilter() { + const seen: {[string]: any} = {}; + return function(key) { + if (seen[key]) { + return false; + } + return (seen[key] = true); + }; +} diff --git a/packages/jest-validate/src/index.js b/packages/jest-validate/src/index.js index c17c0c3797b1..eca9c33fb8b5 100644 --- a/packages/jest-validate/src/index.js +++ b/packages/jest-validate/src/index.js @@ -15,8 +15,10 @@ import { } from './utils'; import validate from './validate'; import validateCLIOptions from './validate_cli_options'; +import {MultipleValidOptions} from './condition'; module.exports = { + MultipleValidOptions, ValidationError, createDidYouMeanMessage, format,