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:
Thomas Roberts 2023-04-11 09:50:59 +01:00 committed by GitHub
parent d35254f537
commit e30c0f5463
4 changed files with 191 additions and 4 deletions

View File

@ -128,6 +128,7 @@ export const TotalsCoupon = ( {
setCouponValue( newCouponValue ); setCouponValue( newCouponValue );
} } } }
focusOnMount={ true } focusOnMount={ true }
validateOnMount={ false }
showError={ false } showError={ false }
/> />
<Button <Button

View File

@ -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();
} );
} );

View File

@ -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();
} );
} ); } );
} ); } );

View File

@ -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();
} }
// if validateOnMount is false, only validate input if focusOnMount is also false
if ( validateOnMount || ! focusOnMount ) {
validateInput( true ); 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( () => {