diff --git a/lib/FundDistribution/FundDistributionFields/FundDistributionFieldsFinal.js b/lib/FundDistribution/FundDistributionFields/FundDistributionFieldsFinal.js index 6c2f8ac4..966c2ec6 100644 --- a/lib/FundDistribution/FundDistributionFields/FundDistributionFieldsFinal.js +++ b/lib/FundDistribution/FundDistributionFields/FundDistributionFieldsFinal.js @@ -52,6 +52,7 @@ const FundDistributionFieldsFinal = ({ }) => { const intl = useIntl(); const [remainingAmount, setRemainingAmount] = useState(0); + const [hasValidationError, setValidationError] = useState(false); const fundsForSelect = useMemo(() => { const activeFunds = funds.filter(({ fundStatus }) => fundStatus === 'Active'); @@ -109,12 +110,16 @@ const FundDistributionFieldsFinal = ({ validateFundDistributionUniqueFunds(records), ].filter(Boolean); - return required + const error = required ? errors[0] : undefined; + + setValidationError(Boolean(error)); + + return error; } - return undefined; + return setValidationError(false); }, [ currency, fundDistribution, @@ -187,6 +192,7 @@ const FundDistributionFieldsFinal = ({ } @@ -237,20 +243,32 @@ const FundDistributionFieldsFinal = ({ ]); return ( - } - component={RepeatableFieldWithValidation} - id={name} - legend={remainingAmountNode} - name={name} - onAdd={onAdd} - onRemove={onRemove} - canAdd={!disabled} - canRemove={!disabled} - renderField={renderSubForm} - validate={debouncedValidate} - /> + <> + { + hasValidationError && ( + true} + validateFields={[]} + render={() => <>} + /> + ) + } + } + component={RepeatableFieldWithValidation} + id={name} + legend={remainingAmountNode} + name={name} + onAdd={onAdd} + onRemove={onRemove} + canAdd={!disabled} + canRemove={!disabled} + renderField={renderSubForm} + validate={debouncedValidate} + /> + ); }; diff --git a/lib/FundDistribution/FundDistributionFields/FundDistributionFieldsFinal.test.js b/lib/FundDistribution/FundDistributionFields/FundDistributionFieldsFinal.test.js index 077a3093..a96f3072 100644 --- a/lib/FundDistribution/FundDistributionFields/FundDistributionFieldsFinal.test.js +++ b/lib/FundDistribution/FundDistributionFields/FundDistributionFieldsFinal.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import user from '@testing-library/user-event'; @@ -7,8 +7,24 @@ import stripesFinalForm from '@folio/stripes/final-form'; import { FUND_DISTR_TYPE } from '../../constants'; import FundDistributionFieldsFinal from './FundDistributionFieldsFinal'; - -const FUNDS = [{ id: '1', code: 'AFRICAHIST', name: 'african', fundStatus: 'Active' }, { id: '2', code: 'TEST', name: 'test', fundStatus: 'Active' }]; +import { handleValidationErrorResponse } from './handleValidationErrorResponse'; +import { validateFundDistributionTotal as validateFundDistributionTotalDefault } from './validateFundDistributionFinal'; + +jest.useFakeTimers('modern'); +jest.mock('./handleValidationErrorResponse', () => ({ + handleValidationErrorResponse: jest.fn(() => 'error message'), +})); +jest.mock('./validateFundDistributionFinal', () => ({ + ...jest.requireActual('./validateFundDistributionFinal'), + validateFundDistributionTotal: jest.fn(() => Promise.resolve()), +})); + +const DELAY = 500; +const validateFundDistributionTotal = jest.fn(() => Promise.resolve()); +const FUNDS = [ + { id: '1', code: 'AFRICAHIST', name: 'african', fundStatus: 'Active' }, + { id: '2', code: 'TEST', name: 'test', fundStatus: 'Active' }, +]; const inactiveFund = { name: 'TestName', code: 'TestCode', @@ -18,7 +34,13 @@ const inactiveFund = { }; // eslint-disable-next-line react/prop-types -const renderForm = ({ fundDistribution, onSelectFund = () => { }, totalAmount = 0, funds = FUNDS }) => ( +const renderForm = ({ + fundDistribution, + onSelectFund = () => { }, + totalAmount = 0, + funds = FUNDS, + ...rest +}) => (
{ }, totalAmount = name="fund-distribution" onSelectFund={onSelectFund} totalAmount={totalAmount} + {...rest} /> ); @@ -42,6 +65,12 @@ const renderComponent = (props = {}) => (render( )); describe('FundDistributionFieldsFinal', () => { + beforeEach(() => { + handleValidationErrorResponse.mockClear(); + validateFundDistributionTotal.mockClear(); + validateFundDistributionTotalDefault.mockClear(); + }); + it('should add new record and call select fund', () => { const onSelectFund = jest.fn(); @@ -53,9 +82,10 @@ describe('FundDistributionFieldsFinal', () => { }); it('should display fund distribution and handle clicks', () => { + const expenseClassesByFundId = { [FUNDS[1].id]: [{ id: 'expClassId', name: 'expClassName' }] }; const fundDistribution = [{ code: 'TEST', fundId: '2', value: 100, distributionType: FUND_DISTR_TYPE.percent }]; - renderComponent({ fundDistribution, totalAmount: 5 }); + renderComponent({ fundDistribution, totalAmount: 5, expenseClassesByFundId }); user.click(screen.getByText('stripes-acq-components.fundDistribution.addBtn')); expect(screen.getByText('$5.00')).toBeDefined(); }); @@ -75,4 +105,54 @@ describe('FundDistributionFieldsFinal', () => { expect(screen.getAllByText('TestName (TestCode) - stripes-acq-components.fundDistribution.fundStatus.Inactive')).toBeDefined(); }); + + it('should call fund distribution validator from props', () => { + const fundDistribution = [inactiveFund]; + + renderComponent({ fundDistribution, funds: fundDistribution, validateFundDistributionTotal }); + + const valueInput = screen.getByTestId('fundDistribution-value'); + + user.type(valueInput, '100'); + user.tab(); + + jest.advanceTimersByTime(DELAY * 1.5); + + expect(validateFundDistributionTotal).toHaveBeenCalled(); + }); + + it('should call default fund distribution validator', () => { + const fundDistribution = [inactiveFund]; + + renderComponent({ fundDistribution, funds: fundDistribution, required: false }); + + const valueInput = screen.getByTestId('fundDistribution-value'); + + user.type(valueInput, '100'); + user.tab(); + + jest.advanceTimersByTime(DELAY * 1.5); + + expect(validateFundDistributionTotalDefault).toHaveBeenCalled(); + }); + + it('should call \'handleValidationErrorResponse\' when validator rejected with an error', async () => { + const fundDistribution = [inactiveFund]; + + // eslint-disable-next-line prefer-promise-reject-errors + validateFundDistributionTotal.mockClear().mockReturnValue(Promise.reject('error message')); + + renderComponent({ fundDistribution, funds: fundDistribution, validateFundDistributionTotal }); + + const valueInput = screen.getByTestId('fundDistribution-value'); + + await act(async () => { + user.type(valueInput, '100'); + user.tab(); + + jest.advanceTimersByTime(DELAY * 1.5); + }); + + expect(handleValidationErrorResponse).toHaveBeenCalled(); + }); });