Skip to content

Commit

Permalink
feat: implemented useValidate & tests
Browse files Browse the repository at this point in the history
  • Loading branch information
chesterkmr committed Dec 4, 2024
1 parent 7cb51fe commit 4d2c708
Show file tree
Hide file tree
Showing 10 changed files with 991 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface IValidatorProviderProps<TValue> {

ref?: React.RefObject<IValidatorRef>;
validateOnChange?: boolean;
validateSync?: boolean;
}

export const ValidatorProvider = <TValue,>({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useValidate';
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { IValidationSchema } from '../../../types';

import debounce from 'lodash/debounce';
import { useCallback, useEffect, useState } from 'react';
import { IValidationError } from '../../../types';
import { validate } from '../../../utils/validate';

export interface IUseAsyncValidateParams {
validationDelay?: number;
validateAsync?: boolean;
validateOnChange?: boolean;
abortEarly?: boolean;
}

export const useAsyncValidate = (
context: object,
schema: IValidationSchema[],
params: IUseAsyncValidateParams = {},
) => {
const {
validationDelay = 500,
validateAsync = false,
validateOnChange = true,
abortEarly = false,
} = params;

const [validationErrors, setValidationErrors] = useState<IValidationError[]>(() =>
validateAsync ? validate(context, schema, { abortEarly }) : [],
);

const validateWithDebounce = useCallback(
debounce((context: object, schema: IValidationSchema[], params: IUseAsyncValidateParams) => {
const errors = validate(context, schema, params);
setValidationErrors(errors);
}, validationDelay),
[validationDelay],
);

useEffect(() => {
if (!validateAsync || !validateOnChange) return;

validateWithDebounce(context, schema, { abortEarly });
}, [context, schema, validateAsync, validateOnChange, abortEarly, validateWithDebounce]);

return validationErrors;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { validate } from '../../../utils/validate';
import { useAsyncValidate } from './useAsyncValidate';

// Mock dependencies
vi.mock('../../../utils/validate', () => ({
validate: vi.fn().mockReturnValue([
{
id: 'name',
originId: 'name',
message: ['error'],
invalidValue: 'John',
},
]),
}));

vi.mock('lodash/debounce', () => ({
default: (fn: any) => fn,
}));

describe('useAsyncValidate', () => {
const mockContext = { name: 'John' };
const mockSchema = [{ id: 'name', validators: [], rules: [] }];

beforeEach(() => {
vi.clearAllMocks();
});

it('should initialize with empty validation errors', () => {
const { result } = renderHook(() => useAsyncValidate(mockContext, mockSchema));
expect(result.current).toEqual([]);
});

it('should not validate when validateAsync is false', () => {
renderHook(() => useAsyncValidate(mockContext, mockSchema, { validateAsync: false }));
expect(validate).not.toHaveBeenCalled();
});

it('should not validate when validateOnChange is false', () => {
renderHook(() => useAsyncValidate(mockContext, mockSchema, { validateOnChange: false }));
expect(validate).not.toHaveBeenCalled();
});

it('should validate and set errors when validateAsync and validateOnChange are true', () => {
const { result } = renderHook(() =>
useAsyncValidate(mockContext, mockSchema, {
validateAsync: true,
validateOnChange: true,
}),
);

expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: false });
expect(result.current).toEqual([
{
id: 'name',
originId: 'name',
message: ['error'],
invalidValue: 'John',
},
]);
});

it('should pass abortEarly param to validate function', () => {
renderHook(() =>
useAsyncValidate(mockContext, mockSchema, {
validateAsync: true,
validateOnChange: true,
abortEarly: true,
}),
);

expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: true });
});

it('should revalidate when context changes', () => {
const { rerender } = renderHook(
({ context }) =>
useAsyncValidate(context, mockSchema, {
validateAsync: true,
validateOnChange: true,
}),
{
initialProps: { context: mockContext },
},
);

const newContext = { name: 'Jane' };
rerender({ context: newContext });

expect(validate).toHaveBeenCalledWith(newContext, mockSchema, { abortEarly: false });
});

it('should revalidate when schema changes', () => {
const { rerender } = renderHook(
({ schema }) =>
useAsyncValidate(mockContext, schema, {
validateAsync: true,
validateOnChange: true,
}),
{
initialProps: { schema: mockSchema },
},
);

const newSchema = [{ id: 'email', validators: [], rules: [] }];
rerender({ schema: newSchema });

expect(validate).toHaveBeenCalledWith(mockContext, newSchema, { abortEarly: false });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useCallback, useState } from 'react';
import { IValidationError, IValidationSchema } from '../../../types';
import { validate } from '../../../utils/validate';

export interface IUseManualValidateParams {
abortEarly?: boolean;
}

export const useManualValidate = (
context: object,
schema: IValidationSchema[],
params: IUseManualValidateParams = {},
): [IValidationError[], () => void] => {
const [validationErrors, setValidationErrors] = useState<IValidationError[]>([]);

const { abortEarly = false } = params;

const _validate = useCallback(() => {
const errors = validate(context, schema, { abortEarly });
setValidationErrors(errors);
}, [context, schema, abortEarly]);

return [validationErrors, _validate];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { validate } from '../../../utils/validate';
import { useManualValidate } from './useManualValidate';

vi.mock('../../../utils/validate', () => ({
validate: vi.fn().mockReturnValue([
{
id: 'name',
originId: 'name',
message: ['error'],
invalidValue: 'John',
},
]),
}));

describe('useManualValidate', () => {
const mockContext = { name: 'John' };
const mockSchema = [{ id: 'name', validators: [], rules: [] }];

beforeEach(() => {
vi.clearAllMocks();
});

it('should initialize with empty validation errors', () => {
const { result } = renderHook(() => useManualValidate(mockContext, mockSchema));

expect(result.current.validationErrors).toEqual([]);
});

it('should validate and set errors when validate is called', () => {
const { result } = renderHook(() => useManualValidate(mockContext, mockSchema));

act(() => {
result.current.validate();
});

expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: false });
expect(result.current.validationErrors).toEqual([
{
id: 'name',
originId: 'name',
message: ['error'],
invalidValue: 'John',
},
]);
});

it('should pass abortEarly param to validate function', () => {
const { result } = renderHook(() =>
useManualValidate(mockContext, mockSchema, { abortEarly: true }),
);

act(() => {
result.current.validate();
});

expect(validate).toHaveBeenCalledWith(mockContext, mockSchema, { abortEarly: true });
});

it('should memoize validate callback with correct dependencies', () => {
const { result, rerender } = renderHook(
({ context, schema, params }) => useManualValidate(context, schema, params),
{
initialProps: {
context: mockContext,
schema: mockSchema,
params: { abortEarly: false },
},
},
);

const firstValidate = result.current.validate;

// Rerender with same props
rerender({
context: mockContext,
schema: mockSchema,
params: { abortEarly: false },
});

expect(result.current.validate).toBe(firstValidate);

// Rerender with different context
rerender({
context: { ...mockContext, newField: 'value' } as any,
schema: mockSchema,
params: { abortEarly: false },
});

expect(result.current.validate).not.toBe(firstValidate);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useMemo } from 'react';
import { IValidationSchema } from '../../../types';
import { validate } from '../../../utils/validate';

export interface IUseSyncValidateParams {
abortEarly?: boolean;
validateSync?: boolean;
validateOnChange?: boolean;
}

export const useSyncValidate = (
context: object,
schema: IValidationSchema[],
params: IUseSyncValidateParams = {},
) => {
const { abortEarly = false, validateSync = false, validateOnChange = true } = params;

return useMemo(() => {
if (!validateSync || !validateOnChange) return [];

return validate(context, schema, { abortEarly });
}, [context, schema, abortEarly, validateSync, validateOnChange]);
};
Loading

0 comments on commit 4d2c708

Please sign in to comment.