Skip to content

Commit

Permalink
feat: added clear value on input hide & updates tests
Browse files Browse the repository at this point in the history
  • Loading branch information
chesterkmr committed Dec 19, 2024
1 parent 8996991 commit a8e111a
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useDynamicForm } from '../../context';
import { useElement } from '../../hooks/external';
import { useEvents } from '../../hooks/internal/useEvents';
import { IFormElement } from '../../types';
import { FieldList } from './FieldList';
import { IUseFieldParams, useFieldList } from './hooks/useFieldList';
Expand All @@ -20,6 +21,7 @@ vi.mock('@/components/organisms/Renderer', () => ({
Renderer: () => <div data-testid="mock-renderer">Renderer</div>,
}));

vi.mock('@/components/organisms/Form/DynamicForm/hooks/internal/useEvents/useEvents');
describe('FieldList', () => {
const mockElement = {
id: 'test-field',
Expand All @@ -45,6 +47,12 @@ describe('FieldList', () => {

vi.mocked(useDynamicForm).mockReturnValue({
elementsMap: {},
fieldHelpers: {
getValue: vi.fn(),
setValue: vi.fn(),
clearValue: vi.fn(),
getTouched: vi.fn(),
},
} as any);

vi.mocked(useStack).mockReturnValue({
Expand All @@ -58,6 +66,11 @@ describe('FieldList', () => {
addItem: mockAddItem,
removeItem: mockRemoveItem,
});

vi.mocked(useEvents).mockReturnValue({
sendEvent: vi.fn(),
sendEventAsync: vi.fn(),
} as unknown as ReturnType<typeof useEvents>);
});

describe('test ids', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useClearValueOnUnmount';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useStack } from '@/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider';
import { IFormElement } from '@/components/organisms/Form/DynamicForm/types';
import { useRef } from 'react';
import { useUnmount } from '../../../../internal/useUnmount';
import { useField } from '../../../useField';

export const useClearValueOnUnmount = (element: IFormElement<any, any>, hidden: boolean) => {
const { stack } = useStack();
const { onChange } = useField(element, stack);
const prevHidden = useRef(hidden);

useUnmount(() => {
if (!prevHidden.current && hidden) {
onChange(undefined, true);
}
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useStack } from '@/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider';
import { IFormElement } from '@/components/organisms/Form/DynamicForm/types';
import { renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useUnmount } from '../../../../internal/useUnmount';
import { useField } from '../../../useField';
import { useClearValueOnUnmount } from './useClearValueOnUnmount';

vi.mock('@/components/organisms/Form/DynamicForm/fields/FieldList/providers/StackProvider');
vi.mock('../../../useField');
vi.mock('../../../../internal/useUnmount');

describe('useClearValueOnUnmount', () => {
const mockElement = {
id: 'test-element',
} as IFormElement<any, any>;

const mockOnChange = vi.fn();
const mockUnmountCallback = vi.fn();

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

vi.mocked(useStack).mockReturnValue({ stack: [] });
vi.mocked(useField).mockReturnValue({ onChange: mockOnChange } as any);
vi.mocked(useUnmount).mockImplementation(callback => {
mockUnmountCallback.mockImplementation(callback);
});
});

it('should not clear value when hidden state has not changed', () => {
renderHook(() => useClearValueOnUnmount(mockElement, false));

mockUnmountCallback();

expect(mockOnChange).not.toHaveBeenCalled();
});

it('should not clear value when element was already hidden', () => {
renderHook(() => useClearValueOnUnmount(mockElement, true));

mockUnmountCallback();

expect(mockOnChange).not.toHaveBeenCalled();
});

it('should clear value when element becomes hidden', () => {
const { rerender } = renderHook(({ hidden }) => useClearValueOnUnmount(mockElement, hidden), {
initialProps: { hidden: false },
});

rerender({ hidden: true });
mockUnmountCallback();

expect(mockOnChange).toHaveBeenCalledWith(undefined, true);
});

it('should use stack from useStack hook', () => {
const mockStack = [1, 2, 3];
vi.mocked(useStack).mockReturnValue({ stack: mockStack });

renderHook(() => useClearValueOnUnmount(mockElement, false));

expect(useField).toHaveBeenCalledWith(mockElement, mockStack);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useEvents } from '../../internal/useEvents';
import { useMount } from '../../internal/useMount';
import { useUnmount } from '../../internal/useUnmount';
import { useElementId } from '../useElementId';
import { useClearValueOnUnmount } from './hooks/useClearValueOnUnmount';

export const useElement = <TElements extends string, TParams>(
element: IFormElement<TElements, TParams>,
Expand All @@ -28,6 +29,7 @@ export const useElement = <TElements extends string, TParams>(

useMount(() => sendEvent('onMount'));
useUnmount(() => sendEvent('onUnmount'));
useClearValueOnUnmount(element, isHidden);

return {
id: useElementId(element, stack),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { IRuleExecutionResult, useRuleEngine } from '@/components/organisms/Form/hooks';
import {
IRuleExecutionResult,
useRuleEngine,
} from '@/components/organisms/Form/hooks/useRuleEngine';
import { renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useDynamicForm } from '../../../context';
Expand All @@ -7,6 +10,7 @@ import { useEvents } from '../../internal/useEvents';
import { useMount } from '../../internal/useMount';
import { useUnmount } from '../../internal/useUnmount';
import { useElementId } from '../useElementId';
import { useClearValueOnUnmount } from './hooks/useClearValueOnUnmount';
import { useElement } from './useElement';

vi.mock('@/components/organisms/Form/hooks/useRuleEngine');
Expand All @@ -15,6 +19,7 @@ vi.mock('../../internal/useEvents');
vi.mock('../../internal/useMount');
vi.mock('../../internal/useUnmount');
vi.mock('../useElementId');
vi.mock('./hooks/useClearValueOnUnmount');

describe('useElement', () => {
const mockSendEvent = vi.fn();
Expand All @@ -39,11 +44,12 @@ describe('useElement', () => {
});

vi.mocked(useRuleEngine).mockReturnValue([]);
vi.mocked(useClearValueOnUnmount).mockImplementation(() => undefined);
});

describe('when stack not provided', () => {
it('should return unmodified id and origin id', () => {
const element = { id: 'test-id' } as IFormElement;
const element = { id: 'test-id' } as IFormElement<string, any>;

const { result } = renderHook(() => useElement(element));

Expand All @@ -54,7 +60,7 @@ describe('useElement', () => {

describe('when stack provided', () => {
it('should format id with stack', () => {
const element = { id: 'test-id' } as IFormElement;
const element = { id: 'test-id' } as IFormElement<string, any>;
const stack = [1, 2];

const { result } = renderHook(() => useElement(element, stack));
Expand All @@ -67,30 +73,38 @@ describe('useElement', () => {
describe('when hidden rules provided', () => {
it('should return hidden true when all hidden rules return true', () => {
vi.mocked(useRuleEngine).mockReturnValue([
{ result: true },
{ result: true },
{ result: true, rule: {} },
{ result: true, rule: {} },
] as IRuleExecutionResult[]);

const element = {
id: 'test-id',
hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 1] } }],
} as IFormElement;
} as IFormElement<string, any>;

const { result } = renderHook(() => useElement(element));

expect(result.current.hidden).toBe(true);
expect(useRuleEngine).toHaveBeenCalledWith(
{ test: 1 },
{
rules: element.hidden,
runOnInitialize: true,
executionDelay: 500,
},
);
});

it('should return hidden false when any hidden rule returns false', () => {
vi.mocked(useRuleEngine).mockReturnValue([
{ result: true },
{ result: false },
{ result: true, rule: {} },
{ result: false, rule: {} },
] as IRuleExecutionResult[]);

const element = {
id: 'test-id',
hidden: [{ engine: 'json-logic', value: { '==': [{ var: 'test' }, 5] } }],
} as IFormElement;
} as IFormElement<string, any>;

const { result } = renderHook(() => useElement(element));

Expand All @@ -102,7 +116,7 @@ describe('useElement', () => {

const element = {
id: 'test-id',
} as IFormElement;
} as IFormElement<string, any>;

const { result } = renderHook(() => useElement(element));

Expand All @@ -112,7 +126,7 @@ describe('useElement', () => {

describe('lifecycle events', () => {
it('should call sendEvent with onMount on mount', () => {
const element = { id: 'test-id' } as IFormElement;
const element = { id: 'test-id' } as IFormElement<string, any>;

renderHook(() => useElement(element));

Expand All @@ -127,7 +141,7 @@ describe('useElement', () => {
});

it('should call sendEvent with onUnmount on unmount', () => {
const element = { id: 'test-id' } as IFormElement;
const element = { id: 'test-id' } as IFormElement<string, any>;

renderHook(() => useElement(element));

Expand All @@ -140,5 +154,16 @@ describe('useElement', () => {

expect(mockSendEvent).toHaveBeenCalledWith('onUnmount');
});

it('should call useClearValueOnUnmount with element and hidden state', () => {
const element = { id: 'test-id' } as IFormElement<string, any>;
vi.mocked(useRuleEngine).mockReturnValue([
{ result: true, rule: {} },
] as IRuleExecutionResult[]);

renderHook(() => useElement(element));

expect(useClearValueOnUnmount).toHaveBeenCalledWith(element, true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ export const useField = <TValue>(
}, [disabledRulesResult]);

const onChange = useCallback(
<TValue>(value: TValue) => {
<TValue>(value: TValue, ignoreEvent = false) => {
setValue(fieldId, valueDestination, value);
setTouched(fieldId, true);

sendEventAsync('onChange');
if (!ignoreEvent) {
sendEventAsync('onChange');
}
},
[fieldId, valueDestination, setValue, setTouched, sendEventAsync],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IRuleExecutionResult, useRuleEngine } from '@/components/organisms/Form/hooks';
import { renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { IDynamicFormContext, useDynamicForm } from '../../../context';
import { ICommonFieldParams, IFormElement } from '../../../types';
import { useEvents } from '../../internal/useEvents';
Expand Down Expand Up @@ -69,6 +69,12 @@ describe('useField', () => {
} as unknown as IDynamicFormContext<object>);
mockGetValue.mockReturnValue('test-value');
mockGetTouched.mockReturnValue(false);

vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
});

it('should return field state and handlers', () => {
Expand Down Expand Up @@ -118,6 +124,16 @@ describe('useField', () => {
expect(mockSetTouched).toHaveBeenCalledWith('test-field-1-2', true);
expect(mockSendEventAsync).toHaveBeenCalledWith('onChange');
});

it('should not trigger async event when ignoreEvent is true', () => {
const { result } = renderHook(() => useField(mockElement, mockStack));

result.current.onChange('new-value', true);

vi.advanceTimersByTime(550);

expect(mockSendEventAsync).not.toHaveBeenCalled();
});
});

describe('onBlur', () => {
Expand Down

0 comments on commit a8e111a

Please sign in to comment.