v1.0 with SW PWA enabled

This commit is contained in:
Blomios
2026-01-01 17:40:53 +01:00
parent 1c0e22aac1
commit 3c8bebb2ad
29775 changed files with 2197201 additions and 119080 deletions

View File

@ -0,0 +1,89 @@
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import React from 'react';
import { useForm } from 'react-hook-form';
import * as vest from 'vest';
import { vestResolver } from '..';
const USERNAME_REQUIRED_MESSAGE = 'username field is required';
const PASSWORD_SYMBOL_MESSAGE = 'password must contain a symbol';
interface FormData {
username: string;
password: string;
}
const validationSuite = vest.create('form', (data: FormData) => {
vest.test('username', USERNAME_REQUIRED_MESSAGE, () => {
vest.enforce(data.username).isNotEmpty();
});
vest.test('password', PASSWORD_SYMBOL_MESSAGE, () => {
vest.enforce(data.password).isNotEmpty();
});
});
interface Props {
onSubmit: (data: FormData) => void;
}
function TestComponent({ onSubmit }: Props) {
const { register, handleSubmit } = useForm<FormData>({
resolver: vestResolver(validationSuite),
shouldUseNativeValidation: true,
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username')} placeholder="username" />
<input {...register('password')} placeholder="password" />
<button type="submit">submit</button>
</form>
);
}
test("form's native validation with Vest", async () => {
const handleSubmit = vi.fn();
render(<TestComponent onSubmit={handleSubmit} />);
// username
let usernameField = screen.getByPlaceholderText(
/username/i,
) as HTMLInputElement;
expect(usernameField.validity.valid).toBe(true);
expect(usernameField.validationMessage).toBe('');
// password
let passwordField = screen.getByPlaceholderText(
/password/i,
) as HTMLInputElement;
expect(passwordField.validity.valid).toBe(true);
expect(passwordField.validationMessage).toBe('');
await user.click(screen.getByText(/submit/i));
// username
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
expect(usernameField.validity.valid).toBe(false);
expect(usernameField.validationMessage).toBe(USERNAME_REQUIRED_MESSAGE);
// password
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
expect(passwordField.validity.valid).toBe(false);
expect(passwordField.validationMessage).toBe(PASSWORD_SYMBOL_MESSAGE);
await user.type(screen.getByPlaceholderText(/username/i), 'joe');
await user.type(screen.getByPlaceholderText(/password/i), 'password');
// username
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
expect(usernameField.validity.valid).toBe(true);
expect(usernameField.validationMessage).toBe('');
// password
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
expect(passwordField.validity.valid).toBe(true);
expect(passwordField.validationMessage).toBe('');
});

View File

@ -0,0 +1,60 @@
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import React from 'react';
import { useForm } from 'react-hook-form';
import * as vest from 'vest';
import { vestResolver } from '..';
interface FormData {
username: string;
password: string;
}
const validationSuite = vest.create('form', (data: FormData) => {
vest.test('username', 'Username is required', () => {
vest.enforce(data.username).isNotEmpty();
});
vest.test('password', 'Password must contain a symbol', () => {
vest.enforce(data.password).matches(/[^A-Za-z0-9]/);
});
});
interface Props {
onSubmit: (data: FormData) => void;
}
function TestComponent({ onSubmit }: Props) {
const {
register,
formState: { errors },
handleSubmit,
} = useForm<FormData>({
resolver: vestResolver(validationSuite), // 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 Vest and TypeScript's integration", async () => {
const handleSubmit = vi.fn();
render(<TestComponent onSubmit={handleSubmit} />);
expect(screen.queryAllByRole('alert')).toHaveLength(0);
await user.click(screen.getByText(/submit/i));
expect(screen.getByText(/Username is required/i)).toBeVisible();
expect(screen.getByText(/Password must contain a symbol/i)).toBeVisible();
expect(handleSubmit).not.toHaveBeenCalled();
});

View File

@ -0,0 +1,67 @@
import { Field, InternalFieldName } from 'react-hook-form';
import * as vest from 'vest';
export const validationSuite = vest.create('form', (data: any = {}) => {
vest.test('username', 'Username is required', () => {
vest.enforce(data.username).isNotEmpty();
});
vest.test('username', 'Must be longer than 3 chars', () => {
vest.enforce(data.username).longerThan(3);
});
vest.test('deepObject.data', 'deepObject.data is required', () => {
vest.enforce(data.deepObject.data).isNotEmpty();
});
vest.test('password', 'Password is required', () => {
vest.enforce(data.password).isNotEmpty();
});
vest.test('password', 'Password must be at least 5 chars', () => {
vest.enforce(data.password).longerThanOrEquals(5);
});
vest.test('password', 'Password must contain a digit', () => {
vest.enforce(data.password).matches(/[0-9]/);
});
vest.test('password', 'Password must contain a symbol', () => {
vest.enforce(data.password).matches(/[^A-Za-z0-9]/);
});
});
export const validData = {
username: 'asdda',
password: 'asddfg123!',
deepObject: {
data: 'test',
},
};
export const invalidData = {
username: '',
password: 'a',
deepObject: {
data: '',
},
};
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',
},
};

View File

@ -0,0 +1,135 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`vestResolver > should return all the error messages from vestResolver when validation fails and validateAllFieldCriteria set to true 1`] = `
{
"errors": {
"deepObject": {
"data": {
"message": "deepObject.data is required",
"ref": undefined,
"type": "",
"types": {
"0": "deepObject.data is required",
},
},
},
"password": {
"message": "Password must be at least 5 chars",
"ref": {
"name": "password",
},
"type": "",
"types": {
"0": "Password must be at least 5 chars",
},
},
"username": {
"message": "Username is required",
"ref": {
"name": "username",
},
"type": "",
"types": {
"0": "Username is required",
},
},
},
"values": {},
}
`;
exports[`vestResolver > should return all the error messages from vestResolver when validation fails and validateAllFieldCriteria set to true and \`mode: sync\` 1`] = `
{
"errors": {
"deepObject": {
"data": {
"message": "deepObject.data is required",
"ref": undefined,
"type": "",
"types": {
"0": "deepObject.data is required",
},
},
},
"password": {
"message": "Password must be at least 5 chars",
"ref": {
"name": "password",
},
"type": "",
"types": {
"0": "Password must be at least 5 chars",
},
},
"username": {
"message": "Username is required",
"ref": {
"name": "username",
},
"type": "",
"types": {
"0": "Username is required",
},
},
},
"values": {},
}
`;
exports[`vestResolver > should return single error message from vestResolver when validation fails and validateAllFieldCriteria set to false 1`] = `
{
"errors": {
"deepObject": {
"data": {
"message": "deepObject.data is required",
"ref": undefined,
"type": "",
},
},
"password": {
"message": "Password must be at least 5 chars",
"ref": {
"name": "password",
},
"type": "",
},
"username": {
"message": "Username is required",
"ref": {
"name": "username",
},
"type": "",
},
},
"values": {},
}
`;
exports[`vestResolver > should return single error message from vestResolver when validation fails and validateAllFieldCriteria set to false and \`mode: sync\` 1`] = `
{
"errors": {
"deepObject": {
"data": {
"message": "deepObject.data is required",
"ref": undefined,
"type": "",
},
},
"password": {
"message": "Password must be at least 5 chars",
"ref": {
"name": "password",
},
"type": "",
},
"username": {
"message": "Username is required",
"ref": {
"name": "username",
},
"type": "",
},
},
"values": {},
}
`;

View File

@ -0,0 +1,90 @@
import { vestResolver } from '..';
import {
fields,
invalidData,
validData,
validationSuite,
} from './__fixtures__/data';
const shouldUseNativeValidation = false;
describe('vestResolver', () => {
it('should return values from vestResolver when validation pass', async () => {
expect(
await vestResolver(validationSuite)(validData, undefined, {
fields,
shouldUseNativeValidation,
}),
).toEqual({
values: validData,
errors: {},
});
});
it('should return values from vestResolver with `mode: sync` when validation pass', async () => {
expect(
await vestResolver(validationSuite, undefined, {
mode: 'sync',
})(validData, undefined, { fields, shouldUseNativeValidation }),
).toEqual({
values: validData,
errors: {},
});
});
it('should return single error message from vestResolver when validation fails and validateAllFieldCriteria set to false', async () => {
expect(
await vestResolver(validationSuite)(invalidData, undefined, {
fields,
shouldUseNativeValidation,
}),
).toMatchSnapshot();
});
it('should return single error message from vestResolver when validation fails and validateAllFieldCriteria set to false and `mode: sync`', async () => {
expect(
await vestResolver(validationSuite, undefined, {
mode: 'sync',
})(invalidData, undefined, { fields, shouldUseNativeValidation }),
).toMatchSnapshot();
});
it('should return all the error messages from vestResolver when validation fails and validateAllFieldCriteria set to true', async () => {
expect(
await vestResolver(validationSuite)(
invalidData,
{},
{ fields, criteriaMode: 'all', shouldUseNativeValidation },
),
).toMatchSnapshot();
});
it('should return all the error messages from vestResolver when validation fails and validateAllFieldCriteria set to true and `mode: sync`', async () => {
expect(
await vestResolver(validationSuite, undefined, { mode: 'sync' })(
invalidData,
{},
{ fields, criteriaMode: 'all', shouldUseNativeValidation },
),
).toMatchSnapshot();
});
it('should call a suite with values, validated field names and a context as arguments', async () => {
const suite = vi.fn(validationSuite) as any as typeof validationSuite;
await vestResolver(suite)(
validData,
{ some: 'context' },
{
fields: { username: fields.username },
names: ['username'],
shouldUseNativeValidation,
},
);
expect(suite).toHaveBeenCalledTimes(1);
expect(suite).toHaveBeenCalledWith(validData, ['username'], {
some: 'context',
});
});
});

View File

@ -0,0 +1,2 @@
export * from './vest';
export * from './types';

View File

@ -0,0 +1,30 @@
import {
FieldName,
FieldValues,
ResolverOptions,
ResolverResult,
} from 'react-hook-form';
import * as Vest from 'vest';
export type ICreateResult<
TValues extends FieldValues = FieldValues,
TContext = any,
> = ReturnType<
typeof Vest.create<
any,
any,
(values: TValues, names?: FieldName<TValues>[], context?: TContext) => void
>
>;
export type Resolver = <TValues extends FieldValues, TContext>(
schema: ICreateResult<TValues, TContext>,
schemaOptions?: never,
factoryOptions?: { mode?: 'async' | 'sync'; rawValues?: boolean },
) => (
values: TValues,
context: TContext | undefined,
options: ResolverOptions<TValues>,
) => Promise<ResolverResult<TValues>>;
export type VestErrors = Record<string, string[]>;

View File

@ -0,0 +1,51 @@
import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
import { FieldError } from 'react-hook-form';
import promisify from 'vest/promisify';
import type { Resolver, VestErrors } from './types';
const parseErrorSchema = (
vestError: VestErrors,
validateAllFieldCriteria: boolean,
) => {
const errors: Record<string, FieldError> = {};
for (const path in vestError) {
if (!errors[path]) {
errors[path] = { message: vestError[path][0], type: '' };
}
if (validateAllFieldCriteria) {
errors[path].types = vestError[path].reduce<Record<number, string>>(
(acc, message, index) => (acc[index] = message) && acc,
{},
);
}
}
return errors;
};
export const vestResolver: Resolver =
(schema, _, resolverOptions = {}) =>
async (values, context, options) => {
const result =
resolverOptions.mode === 'sync'
? schema(values, options.names, context)
: await promisify(schema)(values, options.names, context);
if (result.hasErrors()) {
return {
values: {},
errors: toNestErrors(
parseErrorSchema(
result.getErrors(),
!options.shouldUseNativeValidation &&
options.criteriaMode === 'all',
),
options,
),
};
}
options.shouldUseNativeValidation && validateFieldsNatively({}, options);
return { values, errors: {} };
};