From a12fb720dff85774c914d5ac2c74db8ce4096287 Mon Sep 17 00:00:00 2001 From: Siarhei Karol Date: Tue, 12 Nov 2024 19:19:41 +0500 Subject: [PATCH] code refactoring and additional unit tests --- .../hooks/useComplexLookupSearchResults.ts | 60 +++++++++++++ .../schema/schemaWithDuplicates.service.ts | 8 +- .../userValues/userValueTypes/simpleLookup.ts | 10 +-- .../ComplexLookupSearchResults.tsx | 58 ++----------- .../useComplesLookupSearchResults.test.ts | 86 +++++++++++++++++++ .../ComplexLookupField.test.tsx | 0 .../ComplexLookupSearchResults.test.tsx | 40 +++++++++ 7 files changed, 203 insertions(+), 59 deletions(-) create mode 100644 src/common/hooks/useComplexLookupSearchResults.ts create mode 100644 src/test/__tests__/common/hooks/useComplesLookupSearchResults.test.ts rename src/test/__tests__/components/{ => ComplexLookupField}/ComplexLookupField.test.tsx (100%) create mode 100644 src/test/__tests__/components/ComplexLookupField/ComplexLookupSearchResults.test.tsx diff --git a/src/common/hooks/useComplexLookupSearchResults.ts b/src/common/hooks/useComplexLookupSearchResults.ts new file mode 100644 index 00000000..474f839a --- /dev/null +++ b/src/common/hooks/useComplexLookupSearchResults.ts @@ -0,0 +1,60 @@ +import { useCallback, useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; +import { useIntl } from 'react-intl'; +import { type Row } from '@components/Table'; +import { useSearchContext } from '@common/hooks/useSearchContext'; +import { ComplexLookupSearchResultsProps } from '@components/ComplexLookupField/ComplexLookupSearchResults'; +import state from '@state'; + +export const useComplexLookupSearchResults = ({ + onTitleClick, + tableConfig, + searchResultsFormatter, +}: ComplexLookupSearchResultsProps) => { + const { onAssignRecord } = useSearchContext(); + const data = useRecoilValue(state.search.data); + const sourceData = useRecoilValue(state.search.sourceData); + const { formatMessage } = useIntl(); + + const applyActionItems = useCallback( + (rows: Row[]): Row[] => + rows?.map(row => { + const formattedRow: Row = { ...row }; + + Object.entries(tableConfig.columns).forEach(([key, column]) => { + formattedRow[key] = { + ...row[key], + children: column.formatter + ? column.formatter({ row, formatMessage, onAssign: onAssignRecord, onTitleClick }) + : row[key].label, + }; + }); + + return formattedRow; + }), + [onAssignRecord, tableConfig], + ); + + const formattedData = useMemo( + () => applyActionItems(searchResultsFormatter(data || [], sourceData || [])), + [applyActionItems, data], + ); + + const listHeader = useMemo( + () => + Object.keys(tableConfig.columns).reduce((accum, key) => { + const { label, position, className } = (tableConfig.columns[key] || {}) as SearchResultsTableColumn; + + accum[key] = { + label: label ? formatMessage({ id: label }) : '', + position: position, + className: className, + }; + + return accum; + }, {} as Row), + [tableConfig], + ); + + return { formattedData, listHeader }; +}; diff --git a/src/common/services/schema/schemaWithDuplicates.service.ts b/src/common/services/schema/schemaWithDuplicates.service.ts index 88bc8904..52178790 100644 --- a/src/common/services/schema/schemaWithDuplicates.service.ts +++ b/src/common/services/schema/schemaWithDuplicates.service.ts @@ -10,8 +10,8 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService constructor( private schema: Map, - private selectedEntriesService: ISelectedEntries, - private entryPropertiesGeneratorService?: IEntryPropertiesGeneratorService, + private readonly selectedEntriesService: ISelectedEntries, + private readonly entryPropertiesGeneratorService?: IEntryPropertiesGeneratorService, ) { this.set(schema); this.isManualDuplication = true; @@ -107,7 +107,7 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService if (!entry || entry.cloneOf) return; const { children } = entry; - let updatedEntryUuid = newUuids?.[index] || uuidv4(); + let updatedEntryUuid = newUuids?.[index] ?? uuidv4(); const isFirstAssociatedEntryElem = parentEntry?.dependsOn && newUuids && index === 0; if (isFirstAssociatedEntryElem) { @@ -126,7 +126,7 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService newUuids, }); - if (controlledByEntry && controlledByEntry?.uuid) { + if (controlledByEntry?.uuid) { this.schema.set(controlledByEntry.uuid, controlledByEntry); } diff --git a/src/common/services/userValues/userValueTypes/simpleLookup.ts b/src/common/services/userValues/userValueTypes/simpleLookup.ts index 6a43cfea..9c854ef8 100644 --- a/src/common/services/userValues/userValueTypes/simpleLookup.ts +++ b/src/common/services/userValues/userValueTypes/simpleLookup.ts @@ -11,8 +11,8 @@ export class SimpleLookupUserValueService extends UserValueType implements IUser private contents?: UserValueContents[]; constructor( - private apiClient: IApiClient, - private cacheService: ILookupCacheService, + private readonly apiClient: IApiClient, + private readonly cacheService: ILookupCacheService, ) { super(); } @@ -60,7 +60,7 @@ export class SimpleLookupUserValueService extends UserValueType implements IUser } this.value = { - uuid: uuid || '', + uuid: uuid ?? '', contents: this.contents, }; @@ -70,7 +70,7 @@ export class SimpleLookupUserValueService extends UserValueType implements IUser private checkDefaultGroupValues(groupUri?: string, itemUri?: string) { if (!groupUri || !itemUri) return false; - return (DEFAULT_GROUP_VALUES as DefaultGroupValues)[groupUri as string]?.value === itemUri; + return (DEFAULT_GROUP_VALUES as DefaultGroupValues)[groupUri]?.value === itemUri; } private generateContentItem({ @@ -100,7 +100,7 @@ export class SimpleLookupUserValueService extends UserValueType implements IUser ?.find( ({ label: optionLabel, value }) => value.uri === mappedUri || value.label === label || optionLabel === label, ); - const selectedLabel = typesMap && itemUri ? loadedOption?.label || itemUri : loadedOption?.label || label; + const selectedLabel = typesMap && itemUri ? (loadedOption?.label ?? itemUri) : (loadedOption?.label ?? label); const contentItem = { label: selectedLabel, diff --git a/src/components/ComplexLookupField/ComplexLookupSearchResults.tsx b/src/components/ComplexLookupField/ComplexLookupSearchResults.tsx index 41b43549..b3c05f6b 100644 --- a/src/components/ComplexLookupField/ComplexLookupSearchResults.tsx +++ b/src/components/ComplexLookupField/ComplexLookupSearchResults.tsx @@ -1,11 +1,8 @@ -import { FC, useCallback, useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { FC } from 'react'; import { TableFlex, type Row } from '@components/Table'; -import { useSearchContext } from '@common/hooks/useSearchContext'; -import state from '@state'; +import { useComplexLookupSearchResults } from '@common/hooks/useComplexLookupSearchResults'; -type ComplexLookupSearchResultsProps = { +export type ComplexLookupSearchResultsProps = { onTitleClick?: (id: string, title?: string, headingType?: string) => void; tableConfig: SearchResultsTableConfig; searchResultsFormatter: (data: any[], sourceData?: SourceDataDTO) => Row[]; @@ -16,50 +13,11 @@ export const ComplexLookupSearchResults: FC = ( tableConfig, searchResultsFormatter, }) => { - const { onAssignRecord } = useSearchContext(); - const data = useRecoilValue(state.search.data); - const sourceData = useRecoilValue(state.search.sourceData); - const { formatMessage } = useIntl(); - - const applyActionItems = useCallback( - (rows: Row[]): Row[] => - rows.map(row => { - const formattedRow: Row = { ...row }; - - Object.entries(tableConfig.columns).forEach(([key, column]) => { - formattedRow[key] = { - ...row[key], - children: column.formatter - ? column.formatter({ row, formatMessage, onAssign: onAssignRecord, onTitleClick }) - : row[key].label, - }; - }); - - return formattedRow; - }), - [onAssignRecord, tableConfig], - ); - - const formattedData = useMemo( - () => applyActionItems(searchResultsFormatter(data || [], sourceData || [])), - [applyActionItems, data], - ); - - const listHeader = useMemo( - () => - Object.keys(tableConfig.columns).reduce((accum, key) => { - const { label, position, className } = (tableConfig.columns[key] || {}) as SearchResultsTableColumn; - - accum[key] = { - label: label ? : '', - position: position, - className: className, - }; - - return accum; - }, {} as Row), - [tableConfig], - ); + const { listHeader, formattedData } = useComplexLookupSearchResults({ + onTitleClick, + tableConfig, + searchResultsFormatter, + }); return (
diff --git a/src/test/__tests__/common/hooks/useComplesLookupSearchResults.test.ts b/src/test/__tests__/common/hooks/useComplesLookupSearchResults.test.ts new file mode 100644 index 00000000..9e0e9f16 --- /dev/null +++ b/src/test/__tests__/common/hooks/useComplesLookupSearchResults.test.ts @@ -0,0 +1,86 @@ +import { useRecoilValue } from 'recoil'; +import { renderHook } from '@testing-library/react'; +import { useSearchContext } from '@common/hooks/useSearchContext'; +import { useComplexLookupSearchResults } from '@common/hooks/useComplexLookupSearchResults'; +import { ComplexLookupSearchResultsProps } from '@components/ComplexLookupField/ComplexLookupSearchResults'; +import { Row } from '@components/Table'; + +jest.mock('recoil'); +jest.mock('@common/hooks/useSearchContext', () => ({ + useSearchContext: jest.fn(), +})); + +const data = [ + { + id: '1', + name: { label: 'Item 1' }, + description: { label: 'Description 1' }, + }, +]; +const sourceData = [ + { + id: '1', + name: 'Item 1', + description: 'Description 1', + }, +]; +const tableConfig = { + columns: { + name: { + label: 'name.label', + position: 1, + formatter: ({ row }: any) => row.name.label, + }, + description: { + label: 'description.label', + position: 2, + }, + }, +}; +const searchResultsFormatter = (data: Row[]) => data; + +describe('useComplesLookupSearchResults', () => { + beforeEach(() => { + (useSearchContext as jest.Mock).mockReturnValue({ + onAssignRecord: jest.fn(), + }); + (useRecoilValue as jest.Mock).mockReturnValueOnce(data).mockReturnValueOnce(sourceData); + }); + + it('returns "formattedData" and "listHeader"', () => { + const props: ComplexLookupSearchResultsProps = { + onTitleClick: jest.fn(), + tableConfig, + searchResultsFormatter, + }; + + const { result } = renderHook(() => useComplexLookupSearchResults(props)); + + expect(result.current.formattedData).toEqual([ + { + id: '1', + name: { + label: 'Item 1', + children: 'Item 1', + }, + description: { + label: 'Description 1', + children: 'Description 1', + }, + }, + ]); + + expect(result.current.listHeader).toEqual({ + name: { + label: 'name.label', + position: 1, + className: undefined, + }, + description: { + label: 'description.label', + position: 2, + className: undefined, + }, + }); + }); +}); diff --git a/src/test/__tests__/components/ComplexLookupField.test.tsx b/src/test/__tests__/components/ComplexLookupField/ComplexLookupField.test.tsx similarity index 100% rename from src/test/__tests__/components/ComplexLookupField.test.tsx rename to src/test/__tests__/components/ComplexLookupField/ComplexLookupField.test.tsx diff --git a/src/test/__tests__/components/ComplexLookupField/ComplexLookupSearchResults.test.tsx b/src/test/__tests__/components/ComplexLookupField/ComplexLookupSearchResults.test.tsx new file mode 100644 index 00000000..56db1850 --- /dev/null +++ b/src/test/__tests__/components/ComplexLookupField/ComplexLookupSearchResults.test.tsx @@ -0,0 +1,40 @@ +import { render } from '@testing-library/react'; +import { useComplexLookupSearchResults } from '@common/hooks/useComplexLookupSearchResults'; +import { ComplexLookupSearchResults } from '@components/ComplexLookupField/ComplexLookupSearchResults'; +import { TableFlex } from '@components/Table'; + +jest.mock('@components/Table'); +jest.mock('@common/hooks/useComplexLookupSearchResults'); + +const listHeader = ['Column 1', 'Column 2']; +const formattedData = [ + { id: '1', values: ['Data 1', 'Data 2'] }, + { id: '2', values: ['Data 3', 'Data 4'] }, +]; + +const onTitleClick = jest.fn(); +const searchResultsFormatter = jest.fn(); +const tableConfig = {} as SearchResultsTableConfig; + +describe('ComplexLookupSearchResults', () => { + it('renders "TableFlex" component with the required props', () => { + (useComplexLookupSearchResults as jest.Mock).mockReturnValue({ + listHeader, + formattedData, + }); + (TableFlex as jest.Mock).mockReturnValue(
Mock TableFlex
); + + render( + , + ); + + expect(TableFlex as jest.Mock).toHaveBeenCalledWith( + { header: listHeader, data: formattedData, className: 'results-list' }, + {}, + ); + }); +});