Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multilple valid types #7207

Merged
merged 2 commits into from
Oct 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ([#7207](https://github.com/facebook/jest/pull/7207))

### Fixes

Expand Down
45 changes: 45 additions & 0 deletions packages/jest-validate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
- it matches the Javascript type of any of arguments passed to `MultipleValidOptions(...)`

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:
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Repeated types within multiple valid examples are coalesced in error report 1`] = `
"<red><bold><bold>●<bold> Validation Error</>:</>
<red></>
<red> Option <bold>\\"foo\\"</> must be of type:</>
<red> <bold><green>string<red></> or <bold><green>number<red></></>
<red> but instead received:</>
<red> <bold><red>boolean<red></></>
<red></>
<red> Example:</>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>\\"foo\\"</></>
<red> }</>
<red></>
<red> or</>
<red></>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>\\"bar\\"</></>
<red> }</>
<red></>
<red> or</>
<red></>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>2</></>
<red> }</>
<red></>"
`;

exports[`displays warning for deprecated config options 1`] = `
"<yellow><bold><bold>●<bold> Deprecation Warning</>:</>
<yellow></>
Expand Down Expand Up @@ -107,6 +134,29 @@ exports[`pretty prints valid config for String 1`] = `
<red></>"
`;

exports[`reports errors nicely when failing with multiple valid options 1`] = `
"<red><bold><bold>●<bold> Validation Error</>:</>
<red></>
<red> Option <bold>\\"foo\\"</> must be of type:</>
<red> <bold><green>string<red></> or <bold><green>array<red></></>
<red> but instead received:</>
<red> <bold><red>number<red></></>
<red></>
<red> Example:</>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>\\"text\\"</></>
<red> }</>
<red></>
<red> or</>
<red></>
<red> {</>
<red> <bold>\\"foo\\"</>: <bold>[</></>
<red><bold> \\"text\\"</></>
<red><bold> ]</></>
<red> }</>
<red></>"
`;

exports[`works with custom deprecations 1`] = `
"<yellow><bold>My Custom Deprecation Warning</>:</>
<yellow></>
Expand Down
60 changes: 60 additions & 0 deletions packages/jest-validate/src/__tests__/validate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'use strict';

import validate from '../validate';
import {MultipleValidOptions} from '../condition';
import jestValidateExampleConfig from '../example_config';
import jestValidateDefaultConfig from '../default_config';

Expand Down Expand Up @@ -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();
});
26 changes: 22 additions & 4 deletions packages/jest-validate/src/condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,31 @@

const toString = Object.prototype.toString;

export default function validationCondition(
option: any,
validOption: any,
): boolean {
const MultipleValidOptionsSymbol = 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 getValues(validOption: any) {
if (
Array.isArray(validOption) &&
validOption.length &&
validOption[0] === MultipleValidOptionsSymbol
) {
return validOption.slice(1);
}
return [validOption];
}

export function validationCondition(option: any, validOption: any): boolean {
return getValues(validOption).some(e => validationConditionSingle(option, e));
}

export function MultipleValidOptions(...args: Array<any>) {
return [MultipleValidOptionsSymbol, ...args];
}
2 changes: 1 addition & 1 deletion packages/jest-validate/src/default_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand Down
26 changes: 20 additions & 6 deletions packages/jest-validate/src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {getValues} from './condition';

export const errorMessage = (
option: string,
Expand All @@ -20,22 +21,35 @@ export const errorMessage = (
options: ValidationOptions,
path?: Array<string>,
): void => {
const conditions = getValues(defaultValue);
const validTypes: Array<string> = Array.from(
new Set(conditions.map(getType)),
);

const message = ` Option ${chalk.bold(
`"${path && path.length > 0 ? path.join('.') + '.' : ''}${option}"`,
)} must be of type:
${chalk.bold.green(getType(defaultValue))}
${validTypes.map(e => chalk.bold.green(e)).join(' or ')}
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<any>) {
return examples.map(
e => ` {
${chalk.bold(`"${option}"`)}: ${chalk.bold(formatPrettyObject(e))}
}`,
).join(`

or

`);
}
2 changes: 2 additions & 0 deletions packages/jest-validate/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down