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

Support multiple errors messages #32

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),
};
}
};