-
-
Notifications
You must be signed in to change notification settings - Fork 162
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
329 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "nope", | ||
"amdName": "hookformResolversNope", | ||
"version": "1.0.0", | ||
"private": true, | ||
"description": "React Hook Form validation resolver: nope", | ||
"main": "dist/nope.js", | ||
"module": "dist/nope.module.js", | ||
"umd:main": "dist/nope.umd.js", | ||
"source": "src/index.ts", | ||
"types": "dist/index.d.ts", | ||
"license": "MIT", | ||
"peerDependencies": { | ||
"react-hook-form": "^7.0.0", | ||
"@hookform/resolvers": "^2.0.0", | ||
"nope-validator": "^0.12.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import React from 'react'; | ||
import { render, screen, act } from '@testing-library/react'; | ||
import user from '@testing-library/user-event'; | ||
import { useForm } from 'react-hook-form'; | ||
import Nope from 'nope-validator'; | ||
import { nopeResolver } from '..'; | ||
|
||
const schema = Nope.object().shape({ | ||
username: Nope.string().required(), | ||
password: Nope.string().required(), | ||
}); | ||
|
||
interface FormData { | ||
unusedProperty: string; | ||
username: string; | ||
password: string; | ||
} | ||
|
||
interface Props { | ||
onSubmit: (data: FormData) => void; | ||
} | ||
|
||
function TestComponent({ onSubmit }: Props) { | ||
const { | ||
register, | ||
formState: { errors }, | ||
handleSubmit, | ||
} = useForm<FormData>({ | ||
resolver: nopeResolver(schema), // Useful to check TypeScript regressions | ||
}); | ||
|
||
return ( | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<input {...register('username')} /> | ||
{errors.username && <span role="alert">{errors.username.message}</span>} | ||
|
||
<input {...register('password')} /> | ||
{errors.password && <span role="alert">{errors.password.message}</span>} | ||
|
||
<button type="submit">submit</button> | ||
</form> | ||
); | ||
} | ||
|
||
test("form's validation with Yup and TypeScript's integration", async () => { | ||
const handleSubmit = jest.fn(); | ||
render(<TestComponent onSubmit={handleSubmit} />); | ||
|
||
expect(screen.queryAllByRole(/alert/i)).toHaveLength(0); | ||
|
||
await act(async () => { | ||
user.click(screen.getByText(/submit/i)); | ||
}); | ||
|
||
expect(screen.getByText(/username is a required field/i)).toBeInTheDocument(); | ||
expect(screen.getByText(/password is a required field/i)).toBeInTheDocument(); | ||
expect(handleSubmit).not.toHaveBeenCalled(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { Field, InternalFieldName } from 'react-hook-form'; | ||
import Nope from 'nope-validator'; | ||
|
||
export const schema = Nope.object().shape({ | ||
username: Nope.string().regex(/^\w+$/).min(2).max(30).required(), | ||
password: Nope.string() | ||
.regex(new RegExp('.*[A-Z].*'), 'One uppercase character') | ||
.regex(new RegExp('.*[a-z].*'), 'One lowercase character') | ||
.regex(new RegExp('.*\\d.*'), 'One number') | ||
.regex( | ||
new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'), | ||
'One special character', | ||
) | ||
.min(8, 'Must be at least 8 characters in length') | ||
.required('New Password is required'), | ||
repeatPassword: Nope.string() | ||
.oneOf([Nope.ref('password')], "Passwords don't match") | ||
.required(), | ||
accessToken: Nope.string(), | ||
birthYear: Nope.number().min(1900).max(2013), | ||
email: Nope.string().email(), | ||
tags: Nope.array().of(Nope.string()).required(), | ||
enabled: Nope.boolean(), | ||
like: Nope.object().shape({ | ||
id: Nope.number().required(), | ||
name: Nope.string().atLeast(4).required(), | ||
}), | ||
}); | ||
|
||
export const validData = { | ||
username: 'Doe', | ||
password: 'Password123_', | ||
repeatPassword: 'Password123_', | ||
birthYear: 2000, | ||
email: '[email protected]', | ||
tags: ['tag1', 'tag2'], | ||
enabled: true, | ||
accessToken: 'accessToken', | ||
like: { | ||
id: 1, | ||
name: 'name', | ||
}, | ||
}; | ||
|
||
export const invalidData = { | ||
password: '___', | ||
email: '', | ||
birthYear: 'birthYear', | ||
like: { id: 'z' }, | ||
tags: [1, 2, 3], | ||
}; | ||
|
||
export const fields: Record<InternalFieldName, Field['_f']> = { | ||
username: { | ||
ref: { name: 'username' }, | ||
name: 'username', | ||
}, | ||
password: { | ||
ref: { name: 'password' }, | ||
name: 'password', | ||
}, | ||
email: { | ||
ref: { name: 'email' }, | ||
name: 'email', | ||
}, | ||
birthday: { | ||
ref: { name: 'birthday' }, | ||
name: 'birthday', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`nopeResolver should return a single error from nopeResolver when validation fails 1`] = ` | ||
Object { | ||
"errors": Object { | ||
"birthYear": Object { | ||
"message": "The field is not a valid number", | ||
"ref": undefined, | ||
}, | ||
"like": Object { | ||
"id": Object { | ||
"message": "The field is not a valid number", | ||
"ref": undefined, | ||
}, | ||
"name": Object { | ||
"message": "This field is required", | ||
"ref": undefined, | ||
}, | ||
}, | ||
"password": Object { | ||
"message": "One uppercase character", | ||
"ref": Object { | ||
"name": "password", | ||
}, | ||
}, | ||
"repeatPassword": Object { | ||
"message": "This field is required", | ||
"ref": undefined, | ||
}, | ||
"tags": Object { | ||
"message": "One or more elements are of invalid type", | ||
"ref": undefined, | ||
}, | ||
"username": Object { | ||
"message": "This field is required", | ||
"ref": Object { | ||
"name": "username", | ||
}, | ||
}, | ||
}, | ||
"values": Object {}, | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/* eslint-disable no-console, @typescript-eslint/ban-ts-comment */ | ||
import { nopeResolver } from '..'; | ||
import { schema, validData, fields, invalidData } from './__fixtures__/data'; | ||
|
||
describe('nopeResolver', () => { | ||
it('should return values from nopeResolver when validation pass', async () => { | ||
const schemaSpy = jest.spyOn(schema, 'validate'); | ||
|
||
const result = await nopeResolver(schema)(validData, undefined, { | ||
fields, | ||
}); | ||
|
||
expect(schemaSpy).toHaveBeenCalledTimes(1); | ||
expect(result).toEqual({ errors: {}, values: validData }); | ||
}); | ||
|
||
it('should return a single error from nopeResolver when validation fails', async () => { | ||
const result = await nopeResolver(schema)(invalidData, undefined, { | ||
fields, | ||
}); | ||
|
||
expect(result).toMatchSnapshot(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './nope'; | ||
export * from './types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { FieldErrors } from 'react-hook-form'; | ||
import { toNestError } from '@hookform/resolvers'; | ||
import type { ShapeErrors } from 'nope-validator/lib/cjs/types'; | ||
import type { Resolver } from './types'; | ||
|
||
const parseErrors = ( | ||
errors: ShapeErrors, | ||
parsedErrors: FieldErrors = {}, | ||
path = '', | ||
) => { | ||
return Object.keys(errors).reduce((acc, key) => { | ||
const _path = path ? `${path}.${key}` : key; | ||
const error = errors[key]; | ||
|
||
if (typeof error === 'string') { | ||
acc[_path] = { | ||
message: error, | ||
}; | ||
} else { | ||
parseErrors(error, acc, _path); | ||
} | ||
|
||
return acc; | ||
}, parsedErrors); | ||
}; | ||
|
||
export const nopeResolver: Resolver = ( | ||
schema, | ||
schemaOptions = { | ||
abortEarly: false, | ||
}, | ||
) => (values, context, options) => { | ||
const result = schema.validate(values, context, schemaOptions) as | ||
| ShapeErrors | ||
| undefined; | ||
|
||
return result | ||
? { values: {}, errors: toNestError(parseErrors(result), options.fields) } | ||
: { values, errors: {} }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { | ||
FieldValues, | ||
ResolverOptions, | ||
ResolverResult, | ||
UnpackNestedValue, | ||
} from 'react-hook-form'; | ||
import type NopeObject from 'nope-validator/lib/cjs/NopeObject'; | ||
|
||
type ValidateOptions = Parameters<NopeObject['validate']>[2]; | ||
type Context = Parameters<NopeObject['validate']>[1]; | ||
|
||
export type Resolver = <T extends NopeObject>( | ||
schema: T, | ||
schemaOptions?: ValidateOptions, | ||
) => <TFieldValues extends FieldValues, TContext extends Context>( | ||
values: UnpackNestedValue<TFieldValues>, | ||
context: TContext | undefined, | ||
options: ResolverOptions<TFieldValues>, | ||
) => ResolverResult<TFieldValues>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.