Skip to content

Commit

Permalink
Support multiple errors messages (#32)
Browse files Browse the repository at this point in the history
* support-multiple-errors-message

* rebase rhf/resolver master

* take feedback comment

* improve parseErrorSchema

* fix early issue

* refactor parseErrorSchema

* remove true because message is non undefined

* improve unit test

* reduce code size by a few bytes

* feat(yup): add validate all field criteria logic on parsed errors

* feat(context): prevent user to not used the yup option context

Co-authored-by: Kotaro Sugawara <[email protected]>
  • Loading branch information
Paul-Vandell and kotarella1110 authored Jul 23, 2020
1 parent 0b5601d commit bafb36d
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 37 deletions.
51 changes: 50 additions & 1 deletion src/__snapshots__/yup.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,52 @@ Object {
}
`;

exports[`yupResolver should get errors 1`] = `
exports[`yupResolver errors should get errors with validate all criteria fields 1`] = `
Object {
"errors": Object {
"age": Object {
"message": "age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).",
"type": "typeError",
"types": Object {
"typeError": "age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).",
},
},
"createdOn": Object {
"message": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.",
"type": "typeError",
"types": Object {
"typeError": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.",
},
},
"foo[0].loose": Object {
"message": "foo[0].loose must be a \`boolean\` type, but the final value was: \`null\`.
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`",
"type": "typeError",
"types": Object {
"typeError": "foo[0].loose must be a \`boolean\` type, but the final value was: \`null\`.
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`",
},
},
"password": Object {
"message": "password is a required field",
"type": "required",
"types": Object {
"matches": Array [
"Lowercase",
"Uppercase",
"Number",
"Special",
],
"min": "password must be at least 8 characters",
"required": "password is a required field",
},
},
},
"values": Object {},
}
`;

exports[`yupResolver errors should get errors without validate all criteria fields 1`] = `
Object {
"errors": Object {
"age": Object {
Expand All @@ -36,6 +81,10 @@ Object {
},
},
],
"password": Object {
"message": "password is a required field",
"type": "required",
},
},
"values": Object {},
}
Expand Down
143 changes: 133 additions & 10 deletions src/yup.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import * as yup from 'yup';
import { yupResolver } from './yup';

Expand Down Expand Up @@ -45,6 +46,14 @@ const schema = yup.object().shape({
name: yup.string().required(),
age: yup.number().required().positive().integer(),
email: yup.string().email(),
password: yup
.string()
.required()
.min(8)
.matches(RegExp('(.*[a-z].*)'), 'Lowercase')
.matches(RegExp('(.*[A-Z].*)'), 'Uppercase')
.matches(RegExp('(.*\\d.*)'), 'Number')
.matches(RegExp('[!@#$%^&*(),.?":{}|<>]'), 'Special'),
website: yup.string().url(),
createdOn: yup.date().default(function () {
return new Date();
Expand All @@ -64,6 +73,7 @@ describe('yupResolver', () => {
const data = {
name: 'jimmy',
age: '24',
password: '[}tehk6Uor',
createdOn: '2014-09-23T19:25:25Z',
foo: [{ yup: true }],
};
Expand All @@ -72,22 +82,13 @@ describe('yupResolver', () => {
values: {
name: 'jimmy',
age: 24,
password: '[}tehk6Uor',
foo: [{ yup: true }],
createdOn: new Date('2014-09-23T19:25:25Z'),
},
});
});

it('should get errors', async () => {
const data = {
name: 2,
age: 'test',
createdOn: null,
foo: [{ loose: null }],
};
expect(await yupResolver(schema)(data)).toMatchSnapshot();
});

it('should pass down the yup context', async () => {
const data = { name: 'eric' };
const context = { min: true };
Expand All @@ -106,6 +107,102 @@ describe('yupResolver', () => {
abortEarly: false,
context,
});
(schemaWithContext.validate as jest.Mock).mockClear();
});

describe('errors', () => {
it('should get errors with validate all criteria fields', async () => {
const data = {
name: 2,
age: 'test',
password: '',
createdOn: null,
foo: [{ loose: null }],
};
const resolve = await yupResolver(schema)(data, {}, true);
expect(resolve).toMatchSnapshot();
expect(resolve.errors['foo[0].loose']).toBeDefined();
expect(resolve.errors['foo[0].loose'].types).toMatchInlineSnapshot(`
Object {
"typeError": "foo[0].loose must be a \`boolean\` type, but the final value was: \`null\`.
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`",
}
`);
expect(resolve.errors.age.types).toMatchInlineSnapshot(`
Object {
"typeError": "age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).",
}
`);
expect(resolve.errors.createdOn.types).toMatchInlineSnapshot(`
Object {
"typeError": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.",
}
`);
expect(resolve.errors.password.types).toMatchInlineSnapshot(`
Object {
"matches": Array [
"Lowercase",
"Uppercase",
"Number",
"Special",
],
"min": "password must be at least 8 characters",
"required": "password is a required field",
}
`);
});

it('should get errors without validate all criteria fields', async () => {
const data = {
name: 2,
age: 'test',
createdOn: null,
foo: [{ loose: null }],
};
const resolve = await yupResolver(schema)(data);
expect(await yupResolver(schema)(data)).toMatchSnapshot();
expect(resolve.errors['foo[0].loose']).toBeUndefined();
expect(resolve.errors.age.types).toBeUndefined();
expect(resolve.errors.createdOn.types).toBeUndefined();
expect(resolve.errors.password.types).toBeUndefined();
});

it('should get error if yup errors has no inner errors', async () => {
const data = {
name: 2,
age: 'test',
createdOn: null,
foo: [{ loose: null }],
};
const resolve = await yupResolver(schema, {
abortEarly: true,
})(data);
expect(resolve.errors).toMatchInlineSnapshot(`
Object {
"createdOn": Object {
"message": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.",
"type": "typeError",
},
}
`);
});

it('should return an empty error result if inner yup validation error has no path', async () => {
const data = { name: '' };
const schemaWithContext = yup.object().shape({
name: yup.string().required(),
});
schemaWithContext.validate = jest.fn().mockRejectedValue({
inner: [{ path: '', message: 'error1', type: 'required' }],
} as yup.ValidationError);
const result = await yupResolver(schemaWithContext)(data);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Object {},
"values": Object {},
}
`);
});
});
});

Expand Down Expand Up @@ -154,4 +251,30 @@ describe('validateWithSchema', () => {
}
`);
});

it('should show a warning log if yup context is used instead only on dev environment', async () => {
console.warn = jest.fn();
process.env.NODE_ENV = 'development';
await yupResolver(
{} as any,
{ context: { noContext: true } } as yup.ValidateOptions,
)({});
expect(console.warn).toHaveBeenCalledWith(
"You should not used the yup options context. Please, use the 'useForm' context object instead",
);
process.env.NODE_ENV = 'test';
(console.warn as jest.Mock).mockClear();
});

it('should not show warning log if yup context is used instead only on production environment', async () => {
console.warn = jest.fn();
process.env.NODE_ENV = 'production';
await yupResolver(
{} as any,
{ context: { noContext: true } } as yup.ValidateOptions,
)({});
expect(console.warn).not.toHaveBeenCalled();
process.env.NODE_ENV = 'test';
(console.warn as jest.Mock).mockClear();
});
});
62 changes: 36 additions & 26 deletions src/yup.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
import { appendErrors, transformToNestObject, Resolver } from 'react-hook-form';
import { Resolver, transformToNestObject } from 'react-hook-form';
import Yup from 'yup';

const parseErrorSchema = (
error: Yup.ValidationError,
validateAllFieldCriteria: boolean,
) =>
Array.isArray(error.inner)
Array.isArray(error.inner) && error.inner.length
? error.inner.reduce(
(previous: Record<string, any>, { path, message, type }) => ({
...previous,
...(path
? previous[path] && validateAllFieldCriteria
(previous: Record<string, any>, { path, message, type }) => {
const previousTypes = (previous[path] && previous[path].types) || {};
return {
...previous,
...(path
? {
[path]: appendErrors(
path,
validateAllFieldCriteria,
previous,
type,
message,
),
}
: {
[path]: previous[path] || {
message,
type,
[path]: {
...(previous[path] || {
message,
type,
}),
...(validateAllFieldCriteria
? {
types: { [type]: message || true },
types: {
...previousTypes,
[type]: previousTypes[type]
? [...[].concat(previousTypes[type]), message]
: message,
},
}
: {}),
},
}
: {}),
}),
: {}),
};
},
{},
)
: {
Expand All @@ -41,7 +41,7 @@ const parseErrorSchema = (

export const yupResolver = <TFieldValues extends Record<string, any>>(
schema: Yup.ObjectSchema | Yup.Lazy,
options: Yup.ValidateOptions = {
options: Omit<Yup.ValidateOptions, 'context'> = {
abortEarly: false,
},
): Resolver<TFieldValues> => async (
Expand All @@ -50,19 +50,29 @@ export const yupResolver = <TFieldValues extends Record<string, any>>(
validateAllFieldCriteria = false,
) => {
try {
if (
(options as Yup.ValidateOptions).context &&
process.env.NODE_ENV === 'development'
) {
// eslint-disable-next-line no-console
console.warn(
"You should not used the yup options context. Please, use the 'useForm' context object instead",
);
}
return {
values: (await schema.validate(values, {
context,
...options,
context,
})) as any,
errors: {},
};
} catch (e) {
const parsedErrors = parseErrorSchema(e, validateAllFieldCriteria);
return {
values: {},
errors: transformToNestObject(
parseErrorSchema(e, validateAllFieldCriteria),
),
errors: validateAllFieldCriteria
? parsedErrors
: transformToNestObject(parsedErrors),
};
}
};

0 comments on commit bafb36d

Please sign in to comment.