Add `validateOnMount` prop to `ValidatedTextInput` (https://github.com/woocommerce/woocommerce-blocks/pull/8889)
* Add validateOnFirstFocus prop * Only run validation on first focus if validateOnFirstFocus is true * Rename validateOnFirstFocus to validateOnMount * Set TotalsCoupon to not validate when the input is mounted * Add tests for validation error handling * Fix test that was not making a good assertion * Add tests for validateOnMount functionality * Clean up validateOnMount logic, make the code more readable & efficient
This commit is contained in:
parent
d35254f537
commit
e30c0f5463
|
@ -128,6 +128,7 @@ export const TotalsCoupon = ( {
|
||||||
setCouponValue( newCouponValue );
|
setCouponValue( newCouponValue );
|
||||||
} }
|
} }
|
||||||
focusOnMount={ true }
|
focusOnMount={ true }
|
||||||
|
validateOnMount={ false }
|
||||||
showError={ false }
|
showError={ false }
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { act, render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { dispatch } from '@wordpress/data';
|
||||||
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { TotalsCoupon } from '..';
|
||||||
|
|
||||||
|
describe( 'TotalsCoupon', () => {
|
||||||
|
it( "Shows a validation error when one is in the wc/store/validation data store and doesn't show one when there isn't", () => {
|
||||||
|
const { rerender } = render( <TotalsCoupon instanceId={ 'coupon' } /> );
|
||||||
|
const openCouponFormButton = screen.getByText( 'Add a coupon' );
|
||||||
|
expect( openCouponFormButton ).toBeInTheDocument();
|
||||||
|
userEvent.click( openCouponFormButton );
|
||||||
|
expect(
|
||||||
|
screen.queryByText( 'Invalid coupon code' )
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const { setValidationErrors } = dispatch( VALIDATION_STORE_KEY );
|
||||||
|
act( () => {
|
||||||
|
setValidationErrors( {
|
||||||
|
coupon: {
|
||||||
|
hidden: false,
|
||||||
|
message: 'Invalid coupon code',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
rerender( <TotalsCoupon instanceId={ 'coupon' } /> );
|
||||||
|
expect( screen.getByText( 'Invalid coupon code' ) ).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -6,12 +6,21 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
import { dispatch, select } from '@wordpress/data';
|
import { dispatch, select } from '@wordpress/data';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
|
import * as wpData from '@wordpress/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { __ValidatedTexInputWithoutId as ValidatedTextInput } from '../validated-text-input';
|
import { __ValidatedTexInputWithoutId as ValidatedTextInput } from '../validated-text-input';
|
||||||
|
|
||||||
|
jest.mock( '@wordpress/data', () => ( {
|
||||||
|
__esModule: true,
|
||||||
|
...jest.requireActual( '@wordpress/data' ),
|
||||||
|
useDispatch: jest.fn().mockImplementation( ( args ) => {
|
||||||
|
return jest.requireActual( '@wordpress/data' ).useDispatch( args );
|
||||||
|
} ),
|
||||||
|
} ) );
|
||||||
|
|
||||||
describe( 'ValidatedTextInput', () => {
|
describe( 'ValidatedTextInput', () => {
|
||||||
it( 'Removes related validation error on change', async () => {
|
it( 'Removes related validation error on change', async () => {
|
||||||
render(
|
render(
|
||||||
|
@ -158,14 +167,141 @@ describe( 'ValidatedTextInput', () => {
|
||||||
onChange={ ( value ) => setInputValue( value ) }
|
onChange={ ( value ) => setInputValue( value ) }
|
||||||
value={ inputValue }
|
value={ inputValue }
|
||||||
label={ 'Test Input' }
|
label={ 'Test Input' }
|
||||||
|
required={ true }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
render( <TestComponent /> );
|
render( <TestComponent /> );
|
||||||
const textInputElement = await screen.getByLabelText( 'Test Input' );
|
const textInputElement = await screen.getByLabelText( 'Test Input' );
|
||||||
|
await userEvent.type( textInputElement, 'test' );
|
||||||
await userEvent.type( textInputElement, '{selectall}{del}' );
|
await userEvent.type( textInputElement, '{selectall}{del}' );
|
||||||
|
await textInputElement.blur();
|
||||||
await expect(
|
await expect(
|
||||||
select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' )
|
screen.queryByText( 'Please enter a valid test input' )
|
||||||
).not.toBe( 'Please enter a valid test input' );
|
).not.toBeNull();
|
||||||
|
} );
|
||||||
|
describe( 'correctly validates on mount', () => {
|
||||||
|
it( 'validates when focusOnMount is true and validateOnMount is not set', async () => {
|
||||||
|
const setValidationErrors = jest.fn();
|
||||||
|
wpData.useDispatch.mockImplementation( ( storeName: string ) => {
|
||||||
|
if ( storeName === VALIDATION_STORE_KEY ) {
|
||||||
|
return {
|
||||||
|
...jest
|
||||||
|
.requireActual( '@wordpress/data' )
|
||||||
|
.useDispatch( storeName ),
|
||||||
|
setValidationErrors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return jest
|
||||||
|
.requireActual( '@wordpress/data' )
|
||||||
|
.useDispatch( storeName );
|
||||||
|
} );
|
||||||
|
|
||||||
|
const TestComponent = () => {
|
||||||
|
const [ inputValue, setInputValue ] = useState( '' );
|
||||||
|
return (
|
||||||
|
<ValidatedTextInput
|
||||||
|
instanceId={ '6' }
|
||||||
|
id={ 'test-input' }
|
||||||
|
onChange={ ( value ) => setInputValue( value ) }
|
||||||
|
value={ inputValue }
|
||||||
|
label={ 'Test Input' }
|
||||||
|
required={ true }
|
||||||
|
focusOnMount={ true }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await render( <TestComponent /> );
|
||||||
|
const textInputElement = await screen.getByLabelText(
|
||||||
|
'Test Input'
|
||||||
|
);
|
||||||
|
await expect( textInputElement ).toHaveFocus();
|
||||||
|
await expect( setValidationErrors ).toHaveBeenCalledWith( {
|
||||||
|
'test-input': {
|
||||||
|
message: 'Please enter a valid test input',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
it( 'validates when focusOnMount is false, regardless of validateOnMount value', async () => {
|
||||||
|
const setValidationErrors = jest.fn();
|
||||||
|
wpData.useDispatch.mockImplementation( ( storeName: string ) => {
|
||||||
|
if ( storeName === VALIDATION_STORE_KEY ) {
|
||||||
|
return {
|
||||||
|
...jest
|
||||||
|
.requireActual( '@wordpress/data' )
|
||||||
|
.useDispatch( storeName ),
|
||||||
|
setValidationErrors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return jest
|
||||||
|
.requireActual( '@wordpress/data' )
|
||||||
|
.useDispatch( storeName );
|
||||||
|
} );
|
||||||
|
|
||||||
|
const TestComponent = ( { validateOnMount = false } ) => {
|
||||||
|
const [ inputValue, setInputValue ] = useState( '' );
|
||||||
|
return (
|
||||||
|
<ValidatedTextInput
|
||||||
|
instanceId={ '6' }
|
||||||
|
id={ 'test-input' }
|
||||||
|
onChange={ ( value ) => setInputValue( value ) }
|
||||||
|
value={ inputValue }
|
||||||
|
label={ 'Test Input' }
|
||||||
|
required={ true }
|
||||||
|
focusOnMount={ true }
|
||||||
|
validateOnMount={ validateOnMount }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const { rerender } = await render( <TestComponent /> );
|
||||||
|
const textInputElement = await screen.getByLabelText(
|
||||||
|
'Test Input'
|
||||||
|
);
|
||||||
|
await expect( textInputElement ).toHaveFocus();
|
||||||
|
await expect( setValidationErrors ).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await rerender( <TestComponent validateOnMount={ true } /> );
|
||||||
|
await expect( textInputElement ).toHaveFocus();
|
||||||
|
await expect( setValidationErrors ).not.toHaveBeenCalled();
|
||||||
|
} );
|
||||||
|
it( 'does not validate when validateOnMount is false and focusOnMount is true', async () => {
|
||||||
|
const setValidationErrors = jest.fn();
|
||||||
|
wpData.useDispatch.mockImplementation( ( storeName: string ) => {
|
||||||
|
if ( storeName === VALIDATION_STORE_KEY ) {
|
||||||
|
return {
|
||||||
|
...jest
|
||||||
|
.requireActual( '@wordpress/data' )
|
||||||
|
.useDispatch( storeName ),
|
||||||
|
setValidationErrors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return jest
|
||||||
|
.requireActual( '@wordpress/data' )
|
||||||
|
.useDispatch( storeName );
|
||||||
|
} );
|
||||||
|
|
||||||
|
const TestComponent = () => {
|
||||||
|
const [ inputValue, setInputValue ] = useState( '' );
|
||||||
|
return (
|
||||||
|
<ValidatedTextInput
|
||||||
|
instanceId={ '6' }
|
||||||
|
id={ 'test-input' }
|
||||||
|
onChange={ ( value ) => setInputValue( value ) }
|
||||||
|
value={ inputValue }
|
||||||
|
label={ 'Test Input' }
|
||||||
|
required={ true }
|
||||||
|
focusOnMount={ true }
|
||||||
|
validateOnMount={ false }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
await render( <TestComponent /> );
|
||||||
|
const textInputElement = await screen.getByLabelText(
|
||||||
|
'Test Input'
|
||||||
|
);
|
||||||
|
await expect( textInputElement ).toHaveFocus();
|
||||||
|
await expect( setValidationErrors ).not.toHaveBeenCalled();
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -49,6 +49,8 @@ interface ValidatedTextInputProps
|
||||||
customValidation?:
|
customValidation?:
|
||||||
| ( ( inputObject: HTMLInputElement ) => boolean )
|
| ( ( inputObject: HTMLInputElement ) => boolean )
|
||||||
| undefined;
|
| undefined;
|
||||||
|
// Whether validation should run when focused - only has an effect when focusOnMount is also true.
|
||||||
|
validateOnMount?: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ValidatedTextInput = ( {
|
const ValidatedTextInput = ( {
|
||||||
|
@ -64,6 +66,7 @@ const ValidatedTextInput = ( {
|
||||||
value = '',
|
value = '',
|
||||||
customValidation,
|
customValidation,
|
||||||
label,
|
label,
|
||||||
|
validateOnMount = true,
|
||||||
...rest
|
...rest
|
||||||
}: ValidatedTextInputProps ): JSX.Element => {
|
}: ValidatedTextInputProps ): JSX.Element => {
|
||||||
const [ isPristine, setIsPristine ] = useState( true );
|
const [ isPristine, setIsPristine ] = useState( true );
|
||||||
|
@ -164,9 +167,20 @@ const ValidatedTextInput = ( {
|
||||||
if ( focusOnMount ) {
|
if ( focusOnMount ) {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
validateInput( true );
|
|
||||||
|
// if validateOnMount is false, only validate input if focusOnMount is also false
|
||||||
|
if ( validateOnMount || ! focusOnMount ) {
|
||||||
|
validateInput( true );
|
||||||
|
}
|
||||||
|
|
||||||
setIsPristine( false );
|
setIsPristine( false );
|
||||||
}, [ focusOnMount, isPristine, setIsPristine, validateInput ] );
|
}, [
|
||||||
|
validateOnMount,
|
||||||
|
focusOnMount,
|
||||||
|
isPristine,
|
||||||
|
setIsPristine,
|
||||||
|
validateInput,
|
||||||
|
] );
|
||||||
|
|
||||||
// Remove validation errors when unmounted.
|
// Remove validation errors when unmounted.
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
|
|
Loading…
Reference in New Issue