From 81f2f9e70b4fcec5e44f7a2634681a8f23b88c10 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Fri, 15 Dec 2023 13:30:26 +0100 Subject: [PATCH] refactor and add tests for list admin --- frontend/packages/shared/src/api/queries.ts | 4 +- .../PartyListDetails/PartyListDetail.test.tsx | 159 ++++++++++++++++++ .../PartyListDetails/PartyListDetail.tsx | 11 +- .../PartyListDetails/PartyListSearch.test.tsx | 68 ++++++++ .../PartyListDetails/PartyListSearch.tsx | 5 +- .../useEnhetsregisterOrganizationQuery.ts | 9 +- .../utils/jsonPatchUtils/jsonPatchUtils.ts | 7 +- 7 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 frontend/resourceadm/components/PartyListDetails/PartyListDetail.test.tsx create mode 100644 frontend/resourceadm/components/PartyListDetails/PartyListSearch.test.tsx diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index cf62bb3eb3b..11f71be9b3a 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -64,7 +64,7 @@ import { buildQueryParams } from 'app-shared/utils/urlUtils'; import { componentSchemaUrl, expressionSchemaUrl, layoutSchemaUrl, newsListUrl, numberFormatSchemaUrl, orgsListUrl } from '../cdn-paths'; import type { JsonSchema } from 'app-shared/types/JsonSchema'; import type { PolicyAction, Policy, PolicySubject } from '@altinn/policy-editor'; -import type { PartyList, PartyListResourceLink, Resource, ResourceListItem, ResourceVersionStatus, Validation } from 'app-shared/types/ResourceAdm'; +import type { BrregOrganizationResult, BrregUnderOrganizationResult, PartyList, PartyListResourceLink, Resource, ResourceListItem, ResourceVersionStatus, Validation } from 'app-shared/types/ResourceAdm'; import type { AppConfig } from 'app-shared/types/AppConfig'; import type { Commit } from 'app-shared/types/Commit'; import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata'; @@ -126,6 +126,8 @@ export const getAltinn2LinkServices = (org: string, environment: string) => get< export const getPartyLists = (org: string, environment: string) => get(partyListsPath(org, environment)); export const getPartyList = (org: string, listId: string, environment: string) => get(partyListPath(org, listId, environment)); export const getResourcePartyLists = (org: string, resourceId: string, environment: string) => get(resourcePartyListsPath(org, resourceId, environment)); +export const getEnheter = (url: string) => get(url); +export const getUnderenheter = (url: string) => get(url); // ProcessEditor export const getBpmnFile = (org: string, app: string) => get(processEditorPath(org, app)); diff --git a/frontend/resourceadm/components/PartyListDetails/PartyListDetail.test.tsx b/frontend/resourceadm/components/PartyListDetails/PartyListDetail.test.tsx new file mode 100644 index 00000000000..6645123c919 --- /dev/null +++ b/frontend/resourceadm/components/PartyListDetails/PartyListDetail.test.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { render as rtlRender, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { act } from 'react-dom/test-utils'; +import { textMock } from '../../../testing/mocks/i18nMock'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { PartyListDetail, PartyListDetailProps } from './PartyListDetail'; +import { ServicesContextProps, ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; + +const mockedNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockedNavigate, +})); + +const testOrg = 'ttd'; +const testEnv = 'tt02'; +const testListeIdentifier = 'listid'; + +const defaultProps = { + org: testOrg, + env: testEnv, + list: { + env: testEnv, + identifier: testListeIdentifier, + name: 'Test-liste', + description: 'Dette er en beskrivelse', + members: [ + { + orgNr: '123456789', + orgName: '', + isUnderenhet: false, + }, + ], + }, + backUrl: '/listadmin', +}; + +const user = userEvent.setup(); + +describe('PartyListDetail', () => { + it('should show special name if name is not found', () => { + render(); + expect(screen.getByText('')).toBeInTheDocument(); + }); + + it('should show message when list is empty', () => { + render({ list: { ...defaultProps.list, members: [] } }); + expect(screen.getByText('Listen inneholder ingen enheter')).toBeInTheDocument(); + }); + + it('should call service to remove member', async () => { + const removePartyListMemberMock = jest.fn(); + render({}, { removePartyListMember: removePartyListMemberMock }); + + const removeButton = screen.getByText('Fjern fra liste'); + await act(() => user.click(removeButton)); + + expect(removePartyListMemberMock).toHaveBeenCalled(); + }); + + it('should call service to add member if member is added back', async () => { + const addPartyListMemberMock = jest.fn(); + render({}, { addPartyListMember: addPartyListMemberMock }); + + const removeButton = screen.getByText('Fjern fra liste'); + await act(() => user.click(removeButton)); + + const reAddButton = screen.getByText('Angre fjern'); + await act(() => user.click(reAddButton)); + + expect(addPartyListMemberMock).toHaveBeenCalled(); + }); + + it('should call service to update name', async () => { + const updatePartyListMock = jest.fn(); + render({}, { updatePartyList: updatePartyListMock }); + + const nameField = screen.getByLabelText('Listenavn'); + await act(() => user.type(nameField, ' endret')); + await act(() => nameField.blur()); + + expect(updatePartyListMock).toHaveBeenCalledWith(testOrg, testListeIdentifier, testEnv, [ + { op: 'replace', path: '/name', value: 'Test-liste endret' }, + ]); + }); + + it('should call service to update description', async () => { + const updatePartyListMock = jest.fn(); + render({}, { updatePartyList: updatePartyListMock }); + + const descriptionField = screen.getByLabelText('Beskrivelse'); + await act(() => user.type(descriptionField, ' endret')); + await act(() => descriptionField.blur()); + + expect(updatePartyListMock).toHaveBeenCalledWith(testOrg, testListeIdentifier, testEnv, [ + { op: 'replace', path: '/description', value: 'Dette er en beskrivelse endret' }, + ]); + }); + + it('should call service to remove description', async () => { + const updatePartyListMock = jest.fn(); + render({}, { updatePartyList: updatePartyListMock }); + + const descriptionField = screen.getByLabelText('Beskrivelse'); + await act(() => user.clear(descriptionField)); + await act(() => descriptionField.blur()); + + expect(updatePartyListMock).toHaveBeenCalledWith(testOrg, testListeIdentifier, testEnv, [ + { op: 'remove', path: '/description' }, + ]); + }); + + it('should navigate back after list is deleted', async () => { + const addPartyListMemberMock = jest.fn(); + render({}, { addPartyListMember: addPartyListMemberMock }); + + const deleteListButton = screen.getByText('Slett liste'); + await act(() => user.click(deleteListButton)); + + const confirmDeleteButton = screen.getAllByText('Slett liste'); + await act(() => user.click(confirmDeleteButton[0])); + + expect(mockedNavigate).toHaveBeenCalledWith('/listadmin'); + }); + + it('should close modal on cancel delete', async () => { + const addPartyListMemberMock = jest.fn(); + render({}, { addPartyListMember: addPartyListMemberMock }); + + const deleteListButton = screen.getByText('Slett liste'); + await act(() => user.click(deleteListButton)); + + const cancelDeleteButton = screen.getByText('Avbryt'); + await act(() => user.click(cancelDeleteButton)); + + expect(screen.queryByText('Bekreft sletting av liste')).not.toBeInTheDocument(); + }); +}); + +const render = ( + props: Partial = {}, + queries: Partial = {}, +) => { + const allQueries: ServicesContextProps = { + ...queriesMock, + ...queries, + }; + + return rtlRender( + + + + + , + ); +}; diff --git a/frontend/resourceadm/components/PartyListDetails/PartyListDetail.tsx b/frontend/resourceadm/components/PartyListDetails/PartyListDetail.tsx index 9d1d054fe14..e3296b79341 100644 --- a/frontend/resourceadm/components/PartyListDetails/PartyListDetail.tsx +++ b/frontend/resourceadm/components/PartyListDetails/PartyListDetail.tsx @@ -23,7 +23,7 @@ import { createReplacePatch } from '../../utils/jsonPatchUtils/jsonPatchUtils'; import { useDeletePartyListMutation } from 'resourceadm/hooks/mutations/useDeletePartyListMutation'; import { PartyListSearch } from './PartyListSearch'; -interface PartyListDetailProps { +export interface PartyListDetailProps { org: string; env: string; list: PartyList; @@ -91,21 +91,24 @@ export const PartyListDetail = ({ }); }; + const closeModal = (): void => { + deleteWarningModalRef.current?.close(); + }; + return (
- deleteWarningModalRef.current?.close()}> + Bekreft sletting av liste Vil du slette denne listen? - -
Tilbake diff --git a/frontend/resourceadm/components/PartyListDetails/PartyListSearch.test.tsx b/frontend/resourceadm/components/PartyListDetails/PartyListSearch.test.tsx new file mode 100644 index 00000000000..b7adcfae69e --- /dev/null +++ b/frontend/resourceadm/components/PartyListDetails/PartyListSearch.test.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { render as rtlRender, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { act } from 'react-dom/test-utils'; +import { textMock } from '../../../testing/mocks/i18nMock'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { PartyListSearch } from './PartyListSearch'; +import { ServicesContextProps, ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; + +const enhetSearchResult = { + _embedded: { + enheter: [{ organisasjonsnummer: '123456789', navn: 'Digdir' }], + }, +}; + +const underenhetSearchResult = { + _embedded: { + underenheter: [{ organisasjonsnummer: '987654321', navn: 'Under Digdir' }], + }, +}; + +const handleAddMemberMock = jest.fn(); + +const defaultProps = { + existingMembers: [ + { + orgNr: '123456789', + orgName: '', + isUnderenhet: false, + }, + ], + handleAddMember: handleAddMemberMock, +}; + +const user = userEvent.setup(); + +describe('PartyListSearch', () => { + it('should call handleAddMember when enhet is selected', async () => { + render(); + + const searchField = screen.getByTestId('enhet-search'); + await act(() => user.type(searchField, 'Digdir')); + + await waitFor(() => screen.findByText('987654321 - Under Digdir')); + const searchResultsButton = screen.getByText('987654321 - Under Digdir'); + await act(() => user.click(searchResultsButton)); + + expect(handleAddMemberMock).toHaveBeenCalled(); + }); +}); + +const render = () => { + const allQueries: ServicesContextProps = { + ...queriesMock, + getEnheter: jest.fn().mockImplementation(() => Promise.resolve(enhetSearchResult)), + getUnderenheter: jest.fn().mockImplementation(() => Promise.resolve(underenhetSearchResult)), + }; + + return rtlRender( + + + + + , + ); +}; diff --git a/frontend/resourceadm/components/PartyListDetails/PartyListSearch.tsx b/frontend/resourceadm/components/PartyListDetails/PartyListSearch.tsx index 5a25d0c50ec..d40ba77e11c 100644 --- a/frontend/resourceadm/components/PartyListDetails/PartyListSearch.tsx +++ b/frontend/resourceadm/components/PartyListDetails/PartyListSearch.tsx @@ -63,11 +63,10 @@ export const PartyListSearch = ({ return (
{ - setSearchText(event.target.value); - }} + onChange={(event) => setSearchText(event.target.value)} /> {(isLoadingEnheterSearch || isLoadingUnderenheterSearch) && debouncedSearchText && ( diff --git a/frontend/resourceadm/hooks/queries/useEnhetsregisterOrganizationQuery.ts b/frontend/resourceadm/hooks/queries/useEnhetsregisterOrganizationQuery.ts index 51c37e85aa8..24924ac8996 100644 --- a/frontend/resourceadm/hooks/queries/useEnhetsregisterOrganizationQuery.ts +++ b/frontend/resourceadm/hooks/queries/useEnhetsregisterOrganizationQuery.ts @@ -1,4 +1,5 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import { QueryKey } from 'app-shared/types/QueryKey'; import { BrregOrganizationResult, @@ -16,9 +17,11 @@ const getQueryUrl = (enhetType: string, search: string) => { export const useEnhetsregisterOrganizationQuery = ( navn: string, ): UseQueryResult => { + const { getEnheter } = useServicesContext(); + return useQuery({ queryKey: [QueryKey.EnhetsregisterOrgenhetSearch, navn], - queryFn: () => get(getQueryUrl('enheter', navn)), + queryFn: () => getEnheter(getQueryUrl('enheter', navn)), enabled: !!navn, }); }; @@ -26,9 +29,11 @@ export const useEnhetsregisterOrganizationQuery = ( export const useEnhetsregisterUnderOrganizationQuery = ( navn: string, ): UseQueryResult => { + const { getUnderenheter } = useServicesContext(); + return useQuery({ queryKey: [QueryKey.EnhetsregisterUnderenhetSearch, navn], - queryFn: () => get(getQueryUrl('underenheter', navn)), + queryFn: () => getUnderenheter(getQueryUrl('underenheter', navn)), enabled: !!navn, }); }; diff --git a/frontend/resourceadm/utils/jsonPatchUtils/jsonPatchUtils.ts b/frontend/resourceadm/utils/jsonPatchUtils/jsonPatchUtils.ts index dfb3011fa44..7f663e180ac 100644 --- a/frontend/resourceadm/utils/jsonPatchUtils/jsonPatchUtils.ts +++ b/frontend/resourceadm/utils/jsonPatchUtils/jsonPatchUtils.ts @@ -1,15 +1,16 @@ export interface JsonPatch { op: 'replace' | 'add' | 'remove'; path: string; - value: string | number; + value?: string | number; } export const createReplacePatch = (diff: T): JsonPatch[] => { return Object.keys(diff).map((key) => { + const isRemove = !diff[key]; return { - op: 'replace', + op: isRemove ? 'remove' : 'replace', path: `/${key}`, - value: diff[key], + ...(!isRemove && { value: diff[key] }), }; }); };