diff --git a/src/common/constants/storage.constants.ts b/src/common/constants/storage.constants.ts deleted file mode 100644 index 726625ea..00000000 --- a/src/common/constants/storage.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const AUTOSAVE_INTERVAL = 60 * 1000; -export const AUTOCLEAR_TIMEOUT = 15 * 60 * 1000; -export const DEFAULT_RECORD_ID = 'marva_new_record'; diff --git a/src/common/helpers/progressBackup.helper.ts b/src/common/helpers/progressBackup.helper.ts deleted file mode 100644 index b0e85cef..00000000 --- a/src/common/helpers/progressBackup.helper.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; -import { DEFAULT_RECORD_ID } from '@common/constants/storage.constants'; - -// TODO: UILD-438 - define default profile that will be used for saving a new record -export const generateRecordBackupKey = ( - profile: string = PROFILE_BFIDS.MONOGRAPH, - recordId: number | string = DEFAULT_RECORD_ID, -) => `${profile}:${recordId}`; diff --git a/src/common/helpers/record.helper.ts b/src/common/helpers/record.helper.ts index 8f25896d..26c41a41 100644 --- a/src/common/helpers/record.helper.ts +++ b/src/common/helpers/record.helper.ts @@ -1,6 +1,3 @@ -import { AUTOCLEAR_TIMEOUT } from '@common/constants/storage.constants'; -import { localStorageService } from '@common/services/storage'; -import { generateRecordBackupKey } from './progressBackup.helper'; import { GROUP_BY_LEVEL, GROUP_CONTENTS_LEVEL, @@ -9,7 +6,6 @@ import { TITLE_CONTAINER_URIS, TYPE_URIS, } from '@common/constants/bibframe.constants'; -import { formatRecord } from './recordFormatting.helper'; import { BFLITE_URI_TO_BLOCK, BFLITE_URIS, @@ -35,76 +31,6 @@ export const getRecordId = (record: RecordEntry | null, selectedBlock?: string, return previewBlock ? (record?.resource?.[block]?.[previewBlock] as any[])?.[0]?.id : record?.resource?.[block]?.id; }; -export const getRecordWithUpdatedID = (record: RecordEntry, id: RecordID) => ({ - resource: { - ...record.resource, - [TYPE_URIS.INSTANCE]: { ...record.resource[TYPE_URIS.INSTANCE], id }, - }, -}); - -export const deleteRecordLocally = (profile: string, recordId?: RecordID) => { - const storageKey = generateRecordBackupKey(profile, recordId); - - localStorageService.delete(storageKey); -}; - -export const generateRecordData = (record: ParsedRecord) => { - return { - createdAt: new Date().getTime(), - data: record, - }; -}; - -export const generateAndSaveRecord = (storageKey: string, record: ParsedRecord) => { - const newRecord = generateRecordData(record); - - localStorageService.serialize(storageKey, newRecord); - - return newRecord; -}; - -export const saveRecordLocally = ({ - profile, - parsedRecord, - record, - selectedRecordBlocks, -}: { - profile: string; - parsedRecord: ParsedRecord; - record: RecordEntry | null; - selectedRecordBlocks?: SelectedRecordBlocks; -}) => { - if (!record) return; - - const recordId = getRecordId(record) as string; - const storageKey = generateRecordBackupKey(profile, recordId); - const formattedRecord = formatRecord({ parsedRecord, record, selectedRecordBlocks }); - const updatedRecord = getRecordWithUpdatedID(formattedRecord as RecordEntry, recordId); - - return generateAndSaveRecord(storageKey, updatedRecord as ParsedRecord); -}; - -export const getSavedRecord = (profile: string, recordId?: RecordID): LocallySavedRecord | null => { - const storageKey = generateRecordBackupKey(profile, recordId); - const savedRecordData = localStorageService.deserialize(storageKey); - - if (savedRecordData && !savedRecordData?.data) { - return generateAndSaveRecord(storageKey, savedRecordData); - } - - return savedRecordData ? autoClearSavedData(savedRecordData, profile, recordId) : null; -}; - -export const autoClearSavedData = (savedRecordData: LocallySavedRecord, profile: string, recordId?: RecordID) => { - const shouldBeCleared = savedRecordData.createdAt + AUTOCLEAR_TIMEOUT <= new Date().getTime(); - - if (!shouldBeCleared) return savedRecordData; - - deleteRecordLocally(profile, recordId); - - return null; -}; - export const checkIdentifierAsValue = (record: Record, uri: string) => { const identifierAsValueSelection = IDENTIFIER_AS_VALUE[uri]; diff --git a/src/common/hooks/useRecordControls.ts b/src/common/hooks/useRecordControls.ts index 97f3abdf..b451acb2 100644 --- a/src/common/hooks/useRecordControls.ts +++ b/src/common/hooks/useRecordControls.ts @@ -7,17 +7,9 @@ import { getGraphIdByExternalId, getRecord, } from '@common/api/records.api'; -import { BibframeEntities, PROFILE_BFIDS } from '@common/constants/bibframe.constants'; +import { BibframeEntities } from '@common/constants/bibframe.constants'; import { StatusType } from '@common/constants/status.constants'; -import { DEFAULT_RECORD_ID } from '@common/constants/storage.constants'; -import { - deleteRecordLocally, - getPrimaryEntitiesFromRecord, - getRecordId, - getSelectedRecordBlocks, - saveRecordLocally, - getSavedRecord, -} from '@common/helpers/record.helper'; +import { getPrimaryEntitiesFromRecord, getRecordId, getSelectedRecordBlocks } from '@common/helpers/record.helper'; import { UserNotificationFactory } from '@common/services/userNotification'; import { PreviewParams, useConfig } from '@common/hooks/useConfig.hook'; import { formatRecord } from '@common/helpers/recordFormatting.helper'; @@ -53,8 +45,12 @@ export const useRecordControls = () => { const { setSelectedProfile } = useProfileState(); const { setIsDuplicateImportedResourceModalOpen, setCurrentlyEditedEntityBfid, setCurrentlyPreviewedEntityBfid } = useUIState(); - const { setRecordStatus, setLastSavedRecordId, setIsRecordEdited: setIsEdited, addStatusMessagesItem } = useStatusState(); - const profile = PROFILE_BFIDS.MONOGRAPH; + const { + setRecordStatus, + setLastSavedRecordId, + setIsRecordEdited: setIsEdited, + addStatusMessagesItem, + } = useStatusState(); const currentRecordId = getRecordId(record); const { getProfiles } = useConfig(); const navigate = useNavigate(); @@ -66,12 +62,7 @@ export const useRecordControls = () => { const { generateRecord } = useRecordGeneration(); const fetchRecord = async (recordId: string, previewParams?: PreviewParams) => { - const profile = PROFILE_BFIDS.MONOGRAPH; - const locallySavedData = getSavedRecord(profile, recordId); - const cachedRecord: RecordEntry | undefined = - locallySavedData && !previewParams ? (locallySavedData.data as RecordEntry) : undefined; - - const recordData = await getRecordAndInitializeParsing({ recordId, cachedRecord }); + const recordData = await getRecordAndInitializeParsing({ recordId }); if (!recordData) return; @@ -98,7 +89,6 @@ export const useRecordControls = () => { shouldSetSearchParams = true, }: SaveRecordProps = {}) => { const parsed = generateRecord(); - const currentRecordId = record?.id; if (!parsed) return; @@ -113,14 +103,13 @@ export const useRecordControls = () => { }) as RecordEntry; const recordId = getRecordId(record, selectedRecordBlocks?.block); - const shouldPostRecord = !recordId || getRecordId(record) === DEFAULT_RECORD_ID || isClone; + const shouldPostRecord = !recordId || isClone; const response = shouldPostRecord ? await postRecord(formattedRecord) : await putRecord(recordId as string, formattedRecord); const parsedResponse = await response.json(); - deleteRecordLocally(profile, currentRecordId as RecordID); dispatchUnblockEvent(); !asRefToNewRecord && setRecord(parsedResponse); @@ -177,14 +166,6 @@ export const useRecordControls = () => { } }; - const saveLocalRecord = () => { - const parsed = generateRecord(); - - if (!parsed) return; - - return saveRecordLocally({ profile, parsedRecord: parsed, record, selectedRecordBlocks }); - }; - const clearRecordState = () => { resetUserValues(); setRecord(null); @@ -205,7 +186,6 @@ export const useRecordControls = () => { if (!currentRecordId) return; await deleteRecordRequest(currentRecordId as unknown as string); - deleteRecordLocally(profile, currentRecordId as unknown as string); discardRecord(); addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.success, 'ld.rdDeleted')); @@ -224,7 +204,9 @@ export const useRecordControls = () => { const contents = record?.resource?.[uriSelector]; if (!contents) { - addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantSelectReferenceContents')); + addStatusMessagesItem?.( + UserNotificationFactory.createMessage(StatusType.error, 'ld.cantSelectReferenceContents'), + ); return navigate(ROUTES.RESOURCE_CREATE.uri); } @@ -248,7 +230,13 @@ export const useRecordControls = () => { } }; - const getRecordAndInitializeParsing = async ({ recordId, cachedRecord, idType, previewParams, errorMessage }: IBaseFetchRecord) => { + const getRecordAndInitializeParsing = async ({ + recordId, + cachedRecord, + idType, + previewParams, + errorMessage, + }: IBaseFetchRecord) => { if (!recordId && !cachedRecord) return; try { @@ -262,7 +250,9 @@ export const useRecordControls = () => { return recordData; } catch (_err) { - addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, errorMessage ?? 'ld.errorFetching')); + addStatusMessagesItem?.( + UserNotificationFactory.createMessage(StatusType.error, errorMessage ?? 'ld.errorFetching'), + ); } }; @@ -301,7 +291,6 @@ export const useRecordControls = () => { return { fetchRecord, saveRecord, - saveLocalRecord, deleteRecord, discardRecord, clearRecordState, diff --git a/src/components/DeleteRecord/DeleteRecord.tsx b/src/components/DeleteRecord/DeleteRecord.tsx index e884ae27..b2fd17ee 100644 --- a/src/components/DeleteRecord/DeleteRecord.tsx +++ b/src/components/DeleteRecord/DeleteRecord.tsx @@ -1,10 +1,8 @@ import { FC, memo } from 'react'; import { FormattedMessage } from 'react-intl'; -import { DEFAULT_RECORD_ID } from '@common/constants/storage.constants'; import { useRecordControls } from '@common/hooks/useRecordControls'; import { ModalDeleteRecord } from '@components/ModalDeleteRecord'; import { useModalControls } from '@common/hooks/useModalControls'; -import { getRecordId } from '@common/helpers/record.helper'; import { useRoutePathPattern } from '@common/hooks/useRoutePathPattern'; import { RESOURCE_URLS } from '@common/constants/routes.constants'; import { checkButtonDisabledState } from '@common/helpers/recordControls.helper'; @@ -21,7 +19,7 @@ const DeleteRecord: FC = () => { const { hasBeenSaved } = useRecordStatus(); const isDisabledForEditPage = checkButtonDisabledState({ resourceRoutePattern, isInitiallyLoaded: !hasBeenSaved, isEdited }) || false; - const isDisabled = !record || getRecordId(record) === DEFAULT_RECORD_ID || isDisabledForEditPage; + const isDisabled = !record || isDisabledForEditPage; return ( <> diff --git a/src/components/EditSection/EditSection.tsx b/src/components/EditSection/EditSection.tsx index 9ce64965..49229df3 100644 --- a/src/components/EditSection/EditSection.tsx +++ b/src/components/EditSection/EditSection.tsx @@ -1,15 +1,12 @@ -import { useEffect, memo, useRef } from 'react'; +import { memo, useRef } from 'react'; import { debounce } from 'lodash'; import classNames from 'classnames'; -import { saveRecordLocally } from '@common/helpers/record.helper'; import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; -import { AUTOSAVE_INTERVAL } from '@common/constants/storage.constants'; import { EDIT_SECTION_CONTAINER_ID } from '@common/constants/uiElements.constants'; import { Fields } from '@components/Fields'; import { Prompt } from '@components/Prompt'; import { useContainerEvents } from '@common/hooks/useContainerEvents'; import { useServicesContext } from '@common/hooks/useServicesContext'; -import { useRecordGeneration } from '@common/hooks/useRecordGeneration'; import { useInputsState, useProfileState, useStatusState, useUIState } from '@src/store'; import { renderDrawComponent } from './renderDrawComponent'; import './EditSection.scss'; @@ -20,34 +17,12 @@ export const EditSection = memo(() => { const { selectedEntriesService } = useServicesContext() as Required; const { selectedProfile, initialSchemaKey } = useProfileState(); const resourceTemplates = selectedProfile?.json.Profile.resourceTemplates; - const { userValues, addUserValuesItem, selectedRecordBlocks, record, selectedEntries, setSelectedEntries } = - useInputsState(); + const { userValues, addUserValuesItem, selectedEntries, setSelectedEntries } = useInputsState(); const { isRecordEdited: isEdited, setIsRecordEdited: setIsEdited } = useStatusState(); const { collapsedEntries, setCollapsedEntries, collapsibleEntries, currentlyEditedEntityBfid } = useUIState(); - const { generateRecord } = useRecordGeneration(); useContainerEvents({ watchEditedState: true }); - useEffect(() => { - if (!isEdited) return; - - const autoSaveRecord = setInterval(() => { - try { - const parsed = generateRecord(); - - if (!parsed) return; - - const profile = PROFILE_BFIDS.MONOGRAPH; - - saveRecordLocally({ profile, parsedRecord: parsed, record, selectedRecordBlocks }); - } catch (error) { - console.error('Unable to automatically save changes:', error); - } - }, AUTOSAVE_INTERVAL); - - return () => clearInterval(autoSaveRecord); - }, [isEdited, userValues]); - const debouncedAddUserValues = useRef( debounce((value: UserValues) => { addUserValuesItem?.(value); diff --git a/src/test/__mocks__/common/hooks/useRecordControls.mock.ts b/src/test/__mocks__/common/hooks/useRecordControls.mock.ts index f0bc2369..8b0d96fb 100644 --- a/src/test/__mocks__/common/hooks/useRecordControls.mock.ts +++ b/src/test/__mocks__/common/hooks/useRecordControls.mock.ts @@ -1,5 +1,4 @@ export const saveRecord = jest.fn(); -export const saveRecordLocally = jest.fn(); export const discardRecord = jest.fn(); export const fetchRecord = jest.fn(); export const clearRecordState = jest.fn(); @@ -13,7 +12,6 @@ jest.mock('@common/hooks/useRecordControls', () => ({ discardRecord, fetchRecord, clearRecordState, - saveRecordLocally, fetchRecordAndSelectEntityValues, fetchExternalRecordForPreview, getRecordAndInitializeParsing, diff --git a/src/test/__tests__/common/helpers/progressBackup.helper.test.ts b/src/test/__tests__/common/helpers/progressBackup.helper.test.ts deleted file mode 100644 index ffa1bb68..00000000 --- a/src/test/__tests__/common/helpers/progressBackup.helper.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { generateRecordBackupKey } from '@common/helpers/progressBackup.helper'; - -describe('progressBackup.helper', () => { - describe('generateRecordBackupKey', () => { - function testGenerateRecordBackupKey( - profile: string | undefined, - recordId: number | string | undefined, - testResult: string, - ) { - const result = generateRecordBackupKey(profile, recordId); - - expect(result).toBe(testResult); - } - - test('returns generated key with default params', () => { - testGenerateRecordBackupKey(undefined, undefined, 'lc:profile:bf2:Monograph:marva_new_record'); - }); - - test('returns generated key with passed profile', () => { - testGenerateRecordBackupKey('testProfile', undefined, 'testProfile:marva_new_record'); - }); - - test('returns generated key with passed recordID', () => { - testGenerateRecordBackupKey(undefined, 'testRecordId', 'lc:profile:bf2:Monograph:testRecordId'); - }); - - test('returns generated key with all passed params', () => { - testGenerateRecordBackupKey('testProfile', 'testRecordId', 'testProfile:testRecordId'); - }); - }); -}); diff --git a/src/test/__tests__/common/helpers/record.helper.test.ts b/src/test/__tests__/common/helpers/record.helper.test.ts index 5cc3743f..c242f4b8 100644 --- a/src/test/__tests__/common/helpers/record.helper.test.ts +++ b/src/test/__tests__/common/helpers/record.helper.test.ts @@ -1,131 +1,15 @@ import { getMockedImportedConstant } from '@src/test/__mocks__/common/constants/constants.mock'; import * as RecordHelper from '@common/helpers/record.helper'; -import * as ProgressBackupHelper from '@common/helpers/progressBackup.helper'; -import { localStorageService } from '@common/services/storage'; -import { AUTOCLEAR_TIMEOUT } from '@common/constants/storage.constants'; import * as BibframeConstants from '@src/common/constants/bibframe.constants'; import * as BibframeMappingConstants from '@src/common/constants/bibframeMapping.constants'; describe('record.helper', () => { const mockBlocksBFLiteConstant = getMockedImportedConstant(BibframeMappingConstants, 'BLOCKS_BFLITE'); - const profile = 'test:profile:id'; - const recordId = 'testRecordId'; - const key = 'testKey'; - const record = { - [profile]: { - Instance: [{}], - }, - }; - const date = 111111111; - const storedRecord = { - createdAt: date, - data: record, - }; const testInstanceUri = 'testInstanceUri'; const mockTypeUriConstant = getMockedImportedConstant(BibframeConstants, 'TYPE_URIS'); mockTypeUriConstant({ INSTANCE: testInstanceUri }); - beforeEach(() => { - jest.spyOn(ProgressBackupHelper, 'generateRecordBackupKey').mockReturnValue(key); - }); - - test('deleteRecordLocally - invokes "localStorageService.delete" with generated key', () => { - localStorageService.delete = jest.fn().mockImplementationOnce(data => data); - - RecordHelper.deleteRecordLocally(profile, recordId); - - expect(localStorageService.delete).toHaveBeenCalledWith(key); - }); - - test('generateRecordData - returns generated data for payload', () => { - jest.spyOn(Date.prototype, 'getTime').mockReturnValue(date); - - const result = RecordHelper.generateRecordData(record); - - expect(result).toEqual(storedRecord); - }); - - test('generateAndSaveRecord - invokes "localStorageService.serialize" and returns generated record', () => { - jest.spyOn(RecordHelper, 'generateRecordData').mockReturnValue(storedRecord); - localStorageService.serialize = jest.fn().mockImplementationOnce((_, record) => record); - - const result = RecordHelper.generateAndSaveRecord(key, record); - - expect(localStorageService.serialize).toHaveBeenCalledWith(key, storedRecord); - expect(result).toEqual(storedRecord); - }); - - test('saveRecordLocally - invokes "generateAndSaveRecord" and returns its result', () => { - const parsedRecord = { [testInstanceUri]: {} }; - const record = { resource: { [testInstanceUri]: {} } }; - const testRecord = { - resource: { [testInstanceUri]: { id: 'testId' } }, - }; - const storedRecord = { - createdAt: date, - data: testRecord, - }; - - jest.spyOn(RecordHelper, 'getRecordWithUpdatedID').mockReturnValue(testRecord); - jest.spyOn(RecordHelper, 'generateAndSaveRecord').mockReturnValue(storedRecord); - - const result = RecordHelper.saveRecordLocally({ profile, parsedRecord, record }); - - expect(RecordHelper.generateAndSaveRecord).toHaveBeenCalledWith(key, testRecord); - expect(result).toEqual(storedRecord); - }); - - describe('getSavedRecord', () => { - test('returns null', () => { - localStorageService.deserialize = jest.fn().mockReturnValue(undefined); - - const result = RecordHelper.getSavedRecord(profile, recordId); - - expect(result).toBeNull(); - }); - - test('invokes "generateAndSaveRecord" and returns its value', () => { - localStorageService.deserialize = jest.fn().mockReturnValue(record); - jest.spyOn(RecordHelper, 'generateAndSaveRecord').mockReturnValue(storedRecord); - - const result = RecordHelper.getSavedRecord(profile, recordId); - - expect(RecordHelper.generateAndSaveRecord).toHaveBeenCalledWith(key, record); - expect(result).toEqual(storedRecord); - }); - - test('invokes "autoClearSavedData" and returns its value', () => { - localStorageService.deserialize = jest.fn().mockReturnValue(storedRecord); - jest.spyOn(RecordHelper, 'autoClearSavedData').mockReturnValue(storedRecord); - - const result = RecordHelper.getSavedRecord(profile, recordId); - - expect(RecordHelper.autoClearSavedData).toHaveBeenCalledWith(storedRecord, profile, recordId); - expect(result).toEqual(storedRecord); - }); - }); - - describe('autoClearSavedData', () => { - test('returns initial data', () => { - jest.spyOn(Date.prototype, 'getTime').mockReturnValue(date); - - const result = RecordHelper.autoClearSavedData(storedRecord, profile, recordId); - - expect(result).toEqual(storedRecord); - }); - - test('invokes "deleteRecordLocally" and returns null', () => { - jest.spyOn(Date.prototype, 'getTime').mockReturnValue(date + AUTOCLEAR_TIMEOUT); - jest.spyOn(RecordHelper, 'deleteRecordLocally'); - - const result = RecordHelper.autoClearSavedData(storedRecord, profile, recordId); - - expect(RecordHelper.deleteRecordLocally).toHaveBeenCalledWith(profile, recordId); - expect(result).toBeNull(); - }); - }); - describe('checkIdentifierAsValue', () => { const mockedIdentifierAsValueConstant = { sampleUri: { diff --git a/src/test/__tests__/common/hooks/useRecordControls.test.ts b/src/test/__tests__/common/hooks/useRecordControls.test.ts new file mode 100644 index 00000000..26dafceb --- /dev/null +++ b/src/test/__tests__/common/hooks/useRecordControls.test.ts @@ -0,0 +1,285 @@ +import { renderHook } from '@testing-library/react'; +import { setInitialGlobalState } from '@src/test/__mocks__/store'; +import * as recordsApi from '@common/api/records.api'; +import * as recordHelper from '@common/helpers/record.helper'; +import { RecordStatus } from '@common/constants/record.constants'; +import { StatusType } from '@common/constants/status.constants'; +import { BibframeEntities } from '@common/constants/bibframe.constants'; +import { ExternalResourceIdType } from '@common/constants/api.constants'; +import { ROUTES } from '@common/constants/routes.constants'; +import { useRecordControls } from '@common/hooks/useRecordControls'; +import { useRecordGeneration } from '@common/hooks/useRecordGeneration'; +import { PreviewParams } from '@common/hooks/useConfig.hook'; +import { UserNotificationFactory } from '@common/services/userNotification'; +import { useInputsStore, useStatusStore } from '@src/store'; + +jest.mock('@common/constants/build.constants', () => ({ IS_EMBEDDED_MODE: false })); + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + useNavigate: () => mockNavigate, + useLocation: () => ({ state: {} }), + useSearchParams: () => [new URLSearchParams(), jest.fn()], +})); + +jest.mock('@common/api/records.api', () => ({ + getRecord: jest.fn(), + postRecord: jest.fn(), + putRecord: jest.fn(), +})); + +jest.mock('@common/helpers/recordFormatting.helper', () => ({ + formatRecord: jest.fn(), +})); + +const mockGenerateRecord = jest.fn(); +jest.mock('@common/hooks/useRecordGeneration', () => ({ + useRecordGeneration: () => ({ + generateRecord: mockGenerateRecord, + }), +})); + +const mockGetProfiles = jest.fn(); +jest.mock('@common/hooks/useConfig.hook', () => ({ + useConfig: () => ({ + getProfiles: mockGetProfiles, + }), +})); + +jest.mock('@common/services/userNotification', () => ({ + UserNotificationFactory: { + createMessage: jest.fn(), + }, +})); + +describe('useRecordControls', () => { + const mockSetRecordStatus = jest.fn(); + const mockApiResponse = { id: 'test-id' }; + + beforeEach(() => { + jest.spyOn(console, 'error').mockReturnValue(); + }); + + describe('saveRecord', () => { + beforeEach(() => { + setInitialGlobalState([ + { + store: useStatusStore, + state: { + setRecordStatus: mockSetRecordStatus, + }, + }, + { + store: useInputsStore, + state: { + selectedRecordBlocks: null, + record: {}, + }, + }, + ]); + + mockGenerateRecord.mockReturnValue({}); + }); + + it('saves record with default props', async () => { + const mockResponse = { json: () => Promise.resolve(mockApiResponse) }; + (recordsApi.postRecord as jest.Mock).mockResolvedValue(mockResponse); + + const { result } = renderHook(() => useRecordControls()); + await result.current.saveRecord(); + + expect(recordsApi.postRecord).toHaveBeenCalled(); + }); + + it('handles save with asRefToNewRecord=true', async () => { + const mockResponse = { json: () => Promise.resolve(mockApiResponse) }; + (recordsApi.postRecord as jest.Mock).mockResolvedValue(mockResponse); + + const { result } = renderHook(() => useRecordControls()); + await result.current.saveRecord({ asRefToNewRecord: true }); + + expect(recordsApi.postRecord).toHaveBeenCalled(); + }); + + it('handles save with isNavigatingBack=false', async () => { + const mockResponse = { json: () => Promise.resolve(mockApiResponse) }; + (recordsApi.putRecord as jest.Mock).mockResolvedValue(mockResponse); + + jest.spyOn(recordHelper, 'getRecordId').mockReturnValue('existing-id'); + + const { result } = renderHook(() => useRecordControls()); + await result.current.saveRecord({ isNavigatingBack: false }); + + expect(recordsApi.putRecord).toHaveBeenCalled(); + }); + + it('handles save errors', async () => { + (recordsApi.postRecord as jest.Mock).mockRejectedValue(new Error('Save failed')); + + const { result } = renderHook(() => useRecordControls()); + await result.current.saveRecord(); + + expect(UserNotificationFactory.createMessage).toHaveBeenCalledWith(StatusType.error, 'ld.cantSaveRd'); + }); + + it('updates record status on successful save', async () => { + const mockResponse = { json: () => Promise.resolve(mockApiResponse) }; + (recordsApi.postRecord as jest.Mock).mockResolvedValue(mockResponse); + + const { result } = renderHook(() => useRecordControls()); + await result.current.saveRecord(); + + expect(mockSetRecordStatus).toHaveBeenCalledWith({ + type: RecordStatus.saveAndClose, + }); + }); + + it('does not save if generateRecord returns null', async () => { + jest.spyOn(useRecordGeneration(), 'generateRecord').mockReturnValue(undefined); + + const { result } = renderHook(() => useRecordControls()); + await result.current.saveRecord(); + + expect(recordsApi.postRecord).not.toHaveBeenCalled(); + expect(recordsApi.putRecord).not.toHaveBeenCalled(); + }); + }); + + describe('fetchRecordAndSelectEntityValues', () => { + it('navigates to create route when contents are not found', async () => { + (recordsApi.getRecord as jest.Mock).mockResolvedValue({ resource: {} }); + + const { result } = renderHook(() => useRecordControls()); + await result.current.fetchRecordAndSelectEntityValues('test-id', BibframeEntities.WORK); + + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.RESOURCE_CREATE.uri); + expect(UserNotificationFactory.createMessage).toHaveBeenCalledWith( + StatusType.error, + 'ld.cantSelectReferenceContents', + ); + }); + + it('handles errors during fetch', async () => { + (recordsApi.getRecord as jest.Mock).mockRejectedValue(new Error('Fetch failed')); + + const { result } = renderHook(() => useRecordControls()); + await result.current.fetchRecordAndSelectEntityValues('test-id', BibframeEntities.WORK); + + expect(UserNotificationFactory.createMessage).toHaveBeenCalledWith(StatusType.error, 'ld.errorFetching'); + }); + + it('returns undefined when the record fetch fails', async () => { + (recordsApi.getRecord as jest.Mock).mockRejectedValue(new Error('Fetch failed')); + + const { result } = renderHook(() => useRecordControls()); + const response = await result.current.fetchRecordAndSelectEntityValues('test-id', BibframeEntities.WORK); + + expect(response).toBeUndefined(); + }); + }); + + describe('getRecordAndInitializeParsing', () => { + const mockRecord = { id: 'test-id', data: 'test data' }; + + it('returns undefined when no recordId and no cachedRecord provided', async () => { + const { result } = renderHook(() => useRecordControls()); + const response = await result.current.getRecordAndInitializeParsing({}); + + expect(response).toBeUndefined(); + expect(recordsApi.getRecord).not.toHaveBeenCalled(); + expect(mockGetProfiles).not.toHaveBeenCalled(); + }); + + it('uses cachedRecord when provided', async () => { + const cachedRecord = { ...mockRecord } as unknown as RecordEntry; + mockGetProfiles.mockResolvedValue(undefined); + + const { result } = renderHook(() => useRecordControls()); + const response = await result.current.getRecordAndInitializeParsing({ + cachedRecord, + }); + + expect(response).toEqual(cachedRecord); + expect(recordsApi.getRecord).not.toHaveBeenCalled(); + expect(mockGetProfiles).toHaveBeenCalledWith({ + record: cachedRecord, + recordId: undefined, + previewParams: undefined, + }); + }); + + it('fetches record when recordId is provided', async () => { + (recordsApi.getRecord as jest.Mock).mockResolvedValue(mockRecord); + mockGetProfiles.mockResolvedValue(undefined); + + const { result } = renderHook(() => useRecordControls()); + const response = await result.current.getRecordAndInitializeParsing({ + recordId: 'test-id', + }); + + expect(response).toEqual(mockRecord); + expect(recordsApi.getRecord).toHaveBeenCalledWith({ + recordId: 'test-id', + idType: undefined, + }); + }); + + it('passes idType to getRecord when provided', async () => { + (recordsApi.getRecord as jest.Mock).mockResolvedValue(mockRecord); + mockGetProfiles.mockResolvedValue(undefined); + + const { result } = renderHook(() => useRecordControls()); + await result.current.getRecordAndInitializeParsing({ + recordId: 'test-id', + idType: ExternalResourceIdType.Inventory, + }); + + expect(recordsApi.getRecord).toHaveBeenCalledWith({ + recordId: 'test-id', + idType: ExternalResourceIdType.Inventory, + }); + }); + + it('handles error with default error message', async () => { + (recordsApi.getRecord as jest.Mock).mockRejectedValue(new Error('API Error')); + + const { result } = renderHook(() => useRecordControls()); + const response = await result.current.getRecordAndInitializeParsing({ + recordId: 'test-id', + }); + + expect(response).toBeUndefined(); + expect(UserNotificationFactory.createMessage).toHaveBeenCalledWith(StatusType.error, 'ld.errorFetching'); + }); + + it('handles error with custom error message', async () => { + (recordsApi.getRecord as jest.Mock).mockRejectedValue(new Error('API Error')); + + const { result } = renderHook(() => useRecordControls()); + await result.current.getRecordAndInitializeParsing({ + recordId: 'test-id', + errorMessage: 'custom.error', + }); + + expect(UserNotificationFactory.createMessage).toHaveBeenCalledWith(StatusType.error, 'custom.error'); + }); + + it('passes preview params to getProfiles', async () => { + const previewParams = { param: 'value' } as unknown as PreviewParams; + (recordsApi.getRecord as jest.Mock).mockResolvedValue(mockRecord); + mockGetProfiles.mockResolvedValue(undefined); + + const { result } = renderHook(() => useRecordControls()); + await result.current.getRecordAndInitializeParsing({ + recordId: 'test-id', + previewParams, + }); + + expect(mockGetProfiles).toHaveBeenCalledWith({ + record: mockRecord, + recordId: 'test-id', + previewParams, + }); + }); + }); +}); diff --git a/src/test/__tests__/components/EditSection.test.tsx b/src/test/__tests__/components/EditSection.test.tsx index 3bd4ce90..c237f4aa 100644 --- a/src/test/__tests__/components/EditSection.test.tsx +++ b/src/test/__tests__/components/EditSection.test.tsx @@ -3,7 +3,6 @@ import '@src/test/__mocks__/common/hooks/useConfig.mock'; import { fireEvent, render, waitFor, within } from '@testing-library/react'; import { RouterProvider, createMemoryRouter } from 'react-router-dom'; import { setInitialGlobalState } from '@src/test/__mocks__/store'; -import * as RecordHelper from '@common/helpers/record.helper'; import { AdvancedFieldType } from '@common/constants/uiControls.constants'; import { ServicesProvider } from '@src/providers'; import { routes } from '@src/App'; @@ -241,22 +240,6 @@ describe('EditSection', () => { await waitFor(async () => expect(await findByDisplayValue('sampleValue')).toBeInTheDocument()); }); - test('saves record locally', () => { - jest.useFakeTimers(); - - const spyRecordHelper = jest.spyOn(RecordHelper, 'saveRecordLocally'); - - const { getByTestId } = renderScreen(); - - fireEvent.change(getByTestId('literal-field'), { target: { value: 'sampleValue' } }); - - expect(spyRecordHelper).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(60 * 1000); - - expect(spyRecordHelper).toHaveBeenCalled(); - }); - describe('duplicate groups', () => { test('duplicates a field group', async () => { const { findByTestId } = renderScreen(); diff --git a/src/test/__tests__/views/Edit.test.tsx b/src/test/__tests__/views/Edit.test.tsx index da69984c..648f57d9 100644 --- a/src/test/__tests__/views/Edit.test.tsx +++ b/src/test/__tests__/views/Edit.test.tsx @@ -4,7 +4,6 @@ import { fetchRecord, clearRecordState } from '@src/test/__mocks__/common/hooks/ import { getMockedImportedConstant } from '@src/test/__mocks__/common/constants/constants.mock'; import { act, render, screen } from '@testing-library/react'; import * as Router from 'react-router-dom'; -import * as recordHelper from '@common/helpers/record.helper'; import * as BibframeConstants from '@src/common/constants/bibframe.constants'; import { Edit } from '@views'; import { useProfileStore } from '@src/store/stores/profile'; @@ -43,9 +42,6 @@ describe('Edit', () => { const testInstanceUri = 'testInstanceUri'; const mockImportedConstant = getMockedImportedConstant(BibframeConstants, 'TYPE_URIS'); mockImportedConstant({ INSTANCE: testInstanceUri }); - const mockContents = { - resource: { [testInstanceUri]: {} }, - }; const renderComponent = (recordState: ProfileEntry | null) => act(async () => { @@ -72,21 +68,13 @@ describe('Edit', () => { expect(fetchRecord).toHaveBeenCalled(); }); - test("gets profiles with saved record and doesn't call fetchRecord", async () => { + test("gets profiles and doesn't call fetchRecord", async () => { jest.spyOn(Router, 'useParams').mockReturnValue({ resourceId: undefined }); - jest.spyOn(recordHelper, 'getSavedRecord').mockReturnValue({ - data: mockContents, - createdAt: 100500, - }); - const testRecord = { - resource: { testInstanceUri: { id: 'testId' } }, - }; - jest.spyOn(recordHelper, 'getRecordWithUpdatedID').mockReturnValue(testRecord); await renderComponent(null); expect(getProfiles).toHaveBeenCalledWith({ - record: testRecord, + record: null, }); expect(fetchRecord).not.toHaveBeenCalled(); expect(clearRecordState).toHaveBeenCalled(); diff --git a/src/views/Edit/Edit.tsx b/src/views/Edit/Edit.tsx index a80be729..bbfa2012 100644 --- a/src/views/Edit/Edit.tsx +++ b/src/views/Edit/Edit.tsx @@ -2,8 +2,6 @@ import { useEffect } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { EditSection } from '@components/EditSection'; import { BibframeEntities, PROFILE_BFIDS } from '@common/constants/bibframe.constants'; -import { DEFAULT_RECORD_ID } from '@common/constants/storage.constants'; -import { getSavedRecord, getRecordWithUpdatedID } from '@common/helpers/record.helper'; import { scrollEntity } from '@common/helpers/pageScrolling.helper'; import { useConfig } from '@common/hooks/useConfig.hook'; import { useRecordControls } from '@common/hooks/useRecordControls'; @@ -65,12 +63,7 @@ export const Edit = () => { clearRecordState(); - const profile = PROFILE_BFIDS.MONOGRAPH; - const savedRecordData = getSavedRecord(profile); - const typedSavedRecord = savedRecordData ? (savedRecordData.data as RecordEntry) : null; - let record = typedSavedRecord - ? getRecordWithUpdatedID(typedSavedRecord, DEFAULT_RECORD_ID) - : (null as unknown as RecordEntry); + let record: RecordEntry | null = null; if (resourceReference) { record = (await fetchRecordAndSelectEntityValues(