import * as React from 'react'; import { FunctionComponent } from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { useFormContext, useWatch } from 'react-hook-form'; import { CoreAdminContext, SourceContextProvider } from '../core'; import { testDataProvider } from '../dataProvider'; import { Form } from './Form'; import { useInput, InputProps, UseInputValue } from './useInput'; import { required } from './validation/validate'; import { DefaultValue } from './useInput.stories'; const Input: FunctionComponent< { children: (props: ReturnType) => React.ReactNode; } & InputProps > = props => { const inputProps = useInput(props); return props.children(inputProps); }; const InputWithCustomOnChange: FunctionComponent< { children: (props: ReturnType) => React.ReactNode; } & InputProps & { setContextValue?: (value: string) => void } > = ({ children, setContextValue, ...props }) => { const { getValues } = useFormContext(); return ( { if (props.onChange) { props.onChange(e); } if (setContextValue) { setContextValue(getValues()[props.source]); } }} > {children} ); }; describe('useInput', () => { it('returns the props needed for an input', () => { let inputProps; render(
{props => { inputProps = props; return
; }} ); expect(inputProps.id).toEqual(':r0:'); expect(inputProps.isRequired).toEqual(true); expect(inputProps.field).toBeDefined(); expect(inputProps.field.name).toEqual('title'); expect(inputProps.field.value).toEqual('A title'); expect(inputProps.fieldState).toBeDefined(); }); it('allows to override the input id', () => { let inputProps; render(
{props => { inputProps = props; return
; }} ); expect(inputProps.id).toEqual('my-title'); expect(inputProps.field).toBeDefined(); expect(inputProps.field.name).toEqual('title'); expect(inputProps.fieldState).toBeDefined(); }); it('allows to extend the input event handlers', () => { const handleBlur = jest.fn(); const handleChange = jest.fn(); render(
{({ id, field }) => { return ( ); }}
); const input = screen.getByLabelText('Title'); fireEvent.change(input, { target: { value: 'A title' }, }); expect(handleChange).toHaveBeenCalled(); fireEvent.blur(input); expect(handleBlur).toHaveBeenCalled(); }); it('custom onChange handler should have access to updated context input value', () => { let targetValue, contextValue; const handleChange = e => { targetValue = e.target.value; }; const setContextValue = value => { contextValue = value; }; render(
{({ id, field }) => ( )}
); const input = screen.getByLabelText('Title'); fireEvent.change(input, { target: { value: 'Changed title' }, }); expect(targetValue).toBe('Changed title'); expect(contextValue).toBe('Changed title'); }); describe('defaultValue', () => { it('applies the defaultValue when input does not have a value', () => { render(); expect(screen.queryByDisplayValue('default value')).not.toBeNull(); }); it('does not apply the defaultValue when input has a value', () => { render(); expect(screen.queryByDisplayValue('default value')).toBeNull(); expect(screen.queryByDisplayValue('initial value')).not.toBeNull(); }); it('does not apply the defaultValue when input has an empty string value', () => { render(); expect(screen.queryByDisplayValue('default value')).toBeNull(); }); it('does not apply the defaultValue when input has a null value', () => { render(); expect(screen.queryByDisplayValue('default value')).toBeNull(); expect(screen.queryByDisplayValue('')).not.toBeNull(); }); it('does not apply the defaultValue when input has a value of 0', () => { render(
{({ id, field }) => { return ( ); }}
); expect(screen.queryByDisplayValue('99')).toBeNull(); }); const BooleanInput = ({ source, defaultValue, }: { source: string; defaultValue?: boolean; }) => ( {() => } ); const BooleanInputValue = ({ source }) => { const values = useFormContext().getValues(); return ( <> {typeof values[source] === 'undefined' ? 'undefined' : values[source] ? 'true' : 'false'} ); }; it('does not change the value if the field is of type checkbox and has no value', () => { render(
); expect(screen.queryByText('undefined')).not.toBeNull(); }); it('applies the defaultValue true when the field is of type checkbox and has no value', () => { render(
); expect(screen.queryByText('true')).not.toBeNull(); }); it('applies the defaultValue false when the field is of type checkbox and has no value', () => { render(
); expect(screen.queryByText('false')).not.toBeNull(); }); it('does not apply the defaultValue true when the field is of type checkbox and has a value', () => { render(
); expect(screen.queryByText('false')).not.toBeNull(); }); it('does not apply the defaultValue false when the field is of type checkbox and has a value', () => { render(
); expect(screen.queryByText('true')).not.toBeNull(); }); }); describe('format', () => { it('should format null values to an empty string to avoid console warnings about controlled/uncontrolled components', () => { let inputProps; render(
{props => { inputProps = props; return
; }} ); expect(inputProps.field.value).toEqual(''); }); it('should format undefined values to an empty string to avoid console warnings about controlled/uncontrolled components', () => { let inputProps; render(
{props => { inputProps = props; return
; }} ); expect(inputProps.field.value).toEqual(''); }); it('should format null default values to an empty string to avoid console warnings about controlled/uncontrolled components', () => { let inputProps; render(
{props => { inputProps = props; return
; }} ); expect(inputProps.field.value).toEqual(''); }); it('should apply the provided format function before passing the value to the real input', () => { render(
`${value} formatted`} source="test" children={({ id, field }) => { return ; }} defaultValue="test" />
); expect(screen.getByDisplayValue('test formatted')).not.toBeNull(); }); }); describe('parse', () => { it('should apply the provided parse function before applying the value from the real input', () => { render(
(value + 1).toString()} source="test" children={({ id, field }) => { return ( <> ); }} />
); fireEvent.click(screen.getByText('Set to 999')); expect(screen.getByDisplayValue('1000')).not.toBeNull(); }); it('should parse empty strings to null by default', async () => { const onSubmit = jest.fn(); render(
{ const value = useWatch({ name: 'test' }); return ( <>
'test' value in form:  {JSON.stringify(value)} ( {typeof value})
); }} />
); fireEvent.click(screen.getByText('Set to empty')); await screen.findByText('null (object)'); }); }); describe('validate', () => { it('calls a custom validator with value, allValues, props', async () => { const validator = jest.fn(); render(
{props => ( )} {() =>
} ); fireEvent.change(await screen.findByTestId('title-input'), { target: { value: 'A new title' }, }); await waitFor(() => { expect(validator).toHaveBeenCalledWith( 'A new title', { title: 'A new title', description: 'A description' }, expect.objectContaining({ defaultValue: 'A title', source: 'title', finalSource: 'title', resource: 'posts', }) ); }); }); it('calls a custom validator with the final source in respect to the SourceContext', async () => { const validator = jest.fn(); render(
`posts.0.${source}`, getLabel: label => label, }} > {props => ( )}
); fireEvent.change(await screen.findByTestId('title-input'), { target: { value: 'A new title' }, }); await waitFor(() => { expect(validator).toHaveBeenCalledWith( 'A new title', { posts: [{ title: 'A new title' }] }, expect.objectContaining({ defaultValue: 'A title', source: 'title', finalSource: 'posts.0.title', resource: 'posts', }) ); }); }); it('should validate and be dirty for inputs that were disabled and re-enabled', async () => { let inputProps: UseInputValue | undefined; const DisabledEnableInput = () => { const [disabled, setDisabled] = React.useState(false); return ( <> {props => { inputProps = props; // Capture the latest props return ( ); }} ); }; render(
); // Initial state assertions expect(inputProps?.fieldState.isDirty).toBe(false); expect(inputProps?.field.disabled).toBe(false); // Disable the input fireEvent.click(screen.getByText('Toggle')); await waitFor(() => { expect(inputProps?.fieldState.isDirty).toBe(false); expect(inputProps?.field.disabled).toBe(true); }); // Re-enable the input fireEvent.click(screen.getByText('Toggle')); await waitFor(() => { expect(inputProps?.fieldState.isDirty).toBe(false); expect(inputProps?.field.disabled).toBe(false); }); // Type in the input fireEvent.change(screen.getByLabelText('Title'), { target: { value: 'A title' }, }); // Assert that the field is now dirty await waitFor(() => { expect(inputProps?.fieldState.isDirty).toBe(true); // Now the input should be dirty expect(inputProps?.field.value).toBe('A title'); }); // Clear the input fireEvent.change(screen.getByLabelText('Title'), { target: { value: '' }, }); // Assert that the field is now dirty and invalid because it is required await waitFor(() => { expect(inputProps?.fieldState.isDirty).toBe(true); // Now the input should be dirty expect(inputProps?.field.value).toBe(''); expect(inputProps?.fieldState.invalid).toBe(true); }); }); }); });