Skip to content

Commit

Permalink
Merge branch 'main' into create-endpoint-for-updating-option-list-id
Browse files Browse the repository at this point in the history
  • Loading branch information
standeren authored Nov 27, 2024
2 parents 0c2d7d5 + 707fe2c commit bd51ebf
Show file tree
Hide file tree
Showing 21 changed files with 431 additions and 183 deletions.
1 change: 1 addition & 0 deletions .github/workflows/lint-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
resource-adm
resource-registry
settings
studio-root
subform
testing
text
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { renderHookWithProviders } from '../../test/mocks';
import { useUpdateSelectedMaskinportenScopesMutation } from './useUpdateSelectedMaskinportenScopesMutation';
import { waitFor } from '@testing-library/react';
import { QueryKey } from 'app-shared/types/QueryKey';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import type { QueryClient } from '@tanstack/react-query';
import { app, org } from '@studio/testing/testids';
import {
type MaskinportenScope,
type MaskinportenScopes,
} from 'app-shared/types/MaskinportenScope';

const scopeMock1: MaskinportenScope = {
scope: 'scope1',
description: 'description1',
};
const scopeMock2: MaskinportenScope = {
scope: 'scope2',
description: 'description2',
};
const maskinportenScopes: MaskinportenScopes = { scopes: [scopeMock1, scopeMock2] };

describe('useUpdateSelectedMaskinportenScopesMutation', () => {
it('calls updateSelectedMaskinportenScopes with correct arguments and payload', async () => {
await renderHook();

expect(queriesMock.updateSelectedMaskinportenScopes).toHaveBeenCalledTimes(1);
expect(queriesMock.updateSelectedMaskinportenScopes).toHaveBeenCalledWith(
org,
app,
maskinportenScopes,
);
});

it('invalidates metadata queries when update is successful', async () => {
const queryClient = createQueryClientMock();
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');

await renderHook({ queryClient });

expect(invalidateQueriesSpy).toHaveBeenCalledTimes(1);
expect(invalidateQueriesSpy).toHaveBeenCalledWith({
queryKey: [QueryKey.SelectedAppScopes, org, app],
});
});
});

const renderHook = async ({
queryClient,
}: {
queryClient?: QueryClient;
} = {}) => {
const result = renderHookWithProviders(
{},
queryClient,
)(() => useUpdateSelectedMaskinportenScopesMutation()).renderHookResult.result;
await waitFor(() => result.current.mutateAsync(maskinportenScopes));
expect(result.current.isSuccess).toBe(true);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { type MaskinportenScopes } from 'app-shared/types/MaskinportenScope';
import { QueryKey } from 'app-shared/types/QueryKey';

export const useUpdateSelectedMaskinportenScopesMutation = () => {
const queryClient = useQueryClient();
const { org, app } = useStudioEnvironmentParams();
const { updateSelectedMaskinportenScopes } = useServicesContext();

return useMutation({
mutationFn: (payload: MaskinportenScopes) =>
updateSelectedMaskinportenScopes(org, app, payload),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: [QueryKey.SelectedAppScopes, org, app] }),
});
};
8 changes: 5 additions & 3 deletions frontend/app-development/hooks/queries/useGetScopesQuery.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { QueryKey } from 'app-shared/types/QueryKey';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { type MaskinportenScope } from 'app-shared/types/MaskinportenScope';
import { type MaskinportenScopes } from 'app-shared/types/MaskinportenScope';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';

export const useGetScopesQuery = () => {
const { org, app } = useStudioEnvironmentParams();
const { getMaskinportenScopes } = useServicesContext();
return useQuery<MaskinportenScope[]>({
return useQuery<MaskinportenScopes>({
queryKey: [QueryKey.AppScopes],
queryFn: () => getMaskinportenScopes(),
queryFn: () => getMaskinportenScopes(org, app),
});
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { QueryKey } from 'app-shared/types/QueryKey';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { type MaskinportenScope } from 'app-shared/types/MaskinportenScope';
import { type MaskinportenScopes } from 'app-shared/types/MaskinportenScope';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';

export const useGetSelectedScopesQuery = () => {
const { org, app } = useStudioEnvironmentParams();
const { getSelectedMaskinportenScopes } = useServicesContext();
return useQuery<MaskinportenScope[]>({
queryKey: [QueryKey.SelectedAppScopes],
queryFn: () => getSelectedMaskinportenScopes(),
return useQuery<MaskinportenScopes>({
queryKey: [QueryKey.SelectedAppScopes, org, app],
queryFn: () => getSelectedMaskinportenScopes(org, app),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import { textMock } from '@studio/testing/mocks/i18nMock';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { renderWithProviders } from 'app-development/test/mocks';
import { type MaskinportenScope } from 'app-shared/types/MaskinportenScope';
import {
type MaskinportenScopes,
type MaskinportenScope,
} from 'app-shared/types/MaskinportenScope';
import userEvent from '@testing-library/user-event';
import { app, org } from '@studio/testing/testids';

const scopeMock1: MaskinportenScope = {
scope: 'scope1',
Expand Down Expand Up @@ -102,8 +106,14 @@ describe('ScopeListContainer', () => {
});

it('should toggle all scopes when "select all" checkbox is clicked', async () => {
const uploadScopesListMock = jest.fn().mockImplementation(() => Promise.resolve());

const user = userEvent.setup();
renderScopeList();
renderScopeList({
queries: {
updateSelectedMaskinportenScopes: uploadScopesListMock,
},
});

const selectAllCheckbox = screen.getByRole('checkbox', {
name: textMock('settings_modal.maskinporten_select_all_scopes'),
Expand All @@ -112,30 +122,48 @@ describe('ScopeListContainer', () => {
expect(selectAllCheckbox).not.toBeChecked();
await user.click(selectAllCheckbox);

expect(screen.getByRole('checkbox', { name: scopeMock1.scope })).toBeChecked();
expect(screen.getByRole('checkbox', { name: scopeMock2.scope })).toBeChecked();
const allScopes: MaskinportenScopes = {
scopes: [...maskinportenScopesMock, ...selectedScopesMock],
};

expect(uploadScopesListMock).toHaveBeenCalledTimes(1);
expect(uploadScopesListMock).toHaveBeenCalledWith(org, app, allScopes);
});

it('should toggle individual scope checkbox when clicked', async () => {
const user = userEvent.setup();
const uploadScopesListMock = jest.fn().mockImplementation(() => Promise.resolve());

renderScopeList();
const user = userEvent.setup();
renderScopeList({
queries: {
updateSelectedMaskinportenScopes: uploadScopesListMock,
},
});

const scopeCheckbox = screen.getByRole('checkbox', { name: scopeMock1.scope });
expect(scopeCheckbox).not.toBeChecked();

await user.click(scopeCheckbox);

expect(scopeCheckbox).toBeChecked();
const allSelectedScopes: MaskinportenScopes = {
scopes: [scopeMock1, ...selectedScopesMock],
};

expect(uploadScopesListMock).toHaveBeenCalledTimes(1);
expect(uploadScopesListMock).toHaveBeenCalledWith(org, app, allSelectedScopes);
});
});

type RenderScopeListProps = {
props?: Partial<ScopeListProps>;
queries?: Partial<typeof queriesMock>;
};

const renderScopeList = ({ props }: Partial<RenderScopeListProps> = {}) => {
const renderScopeList = ({ props, queries = queriesMock }: Partial<RenderScopeListProps> = {}) => {
const queryClient = createQueryClientMock();

renderWithProviders({ ...queriesMock }, queryClient)(<ScopeList {...defaultProps} {...props} />);
renderWithProviders(
{ ...queriesMock, ...queries },
queryClient,
)(<ScopeList {...defaultProps} {...props} />);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { type ChangeEvent, useState, type ReactElement } from 'react';
import React, { type ChangeEvent, type ReactElement } from 'react';
import classes from './ScopeList.module.css';

import {
Expand All @@ -8,7 +8,10 @@ import {
StudioParagraph,
} from '@studio/components';
import { Trans, useTranslation } from 'react-i18next';
import { type MaskinportenScope } from 'app-shared/types/MaskinportenScope';
import {
type MaskinportenScopes,
type MaskinportenScope,
} from 'app-shared/types/MaskinportenScope';
import {
getAllElementsChecked,
getAllElementsDisabled,
Expand All @@ -21,6 +24,7 @@ import {
import { GetInTouchWith } from 'app-shared/getInTouch';
import { EmailContactProvider } from 'app-shared/getInTouch/providers';
import { LoggedInTitle } from '../LoggedInTitle';
import { useUpdateSelectedMaskinportenScopesMutation } from 'app-development/hooks/mutations/useUpdateSelectedMaskinportenScopesMutation';

export type ScopeListProps = {
maskinPortenScopes: MaskinportenScope[];
Expand All @@ -29,6 +33,8 @@ export type ScopeListProps = {

export const ScopeList = ({ maskinPortenScopes, selectedScopes }: ScopeListProps): ReactElement => {
const { t } = useTranslation();
const { mutate: mutateSelectedMaskinportenScopes } =
useUpdateSelectedMaskinportenScopesMutation();

const checkboxTableRowElements: StudioCheckboxTableRowElement[] = mapScopesToRowElements(
maskinPortenScopes,
Expand All @@ -37,17 +43,13 @@ export const ScopeList = ({ maskinPortenScopes, selectedScopes }: ScopeListProps

const contactByEmail = new GetInTouchWith(new EmailContactProvider());

// This useState is temporary to simulate correct behaviour in browser. It will be removed and replaced by a mutation function
const [rowElements, setRowElements] =
useState<StudioCheckboxTableRowElement[]>(checkboxTableRowElements);

const areAllChecked = getAllElementsChecked(rowElements);
const isAnyChecked = getSomeElementsChecked(rowElements);
const allScopesDisabled: boolean = getAllElementsDisabled(rowElements);
const areAllChecked = getAllElementsChecked(checkboxTableRowElements);
const isAnyChecked = getSomeElementsChecked(checkboxTableRowElements);
const allScopesDisabled: boolean = getAllElementsDisabled(checkboxTableRowElements);

const handleChangeAllScopes = () => {
const updatedRowElements: StudioCheckboxTableRowElement[] = updateRowElementsCheckedState(
rowElements,
checkboxTableRowElements,
areAllChecked,
);
saveUpdatedScopes(updatedRowElements);
Expand All @@ -56,18 +58,18 @@ export const ScopeList = ({ maskinPortenScopes, selectedScopes }: ScopeListProps
const handleChangeScope = (event: ChangeEvent<HTMLInputElement>) => {
const selectedScope = event.target.value;
const updatedRowElements: StudioCheckboxTableRowElement[] = toggleRowElementCheckedState(
rowElements,
checkboxTableRowElements,
selectedScope,
);
saveUpdatedScopes(updatedRowElements);
};

const saveUpdatedScopes = (updatedRowElements: StudioCheckboxTableRowElement[]) => {
const updatedScopes: MaskinportenScope[] = mapRowElementsToSelectedScopes(updatedRowElements);
console.log('SelectedScopes', updatedScopes);
const updatedScopeList: MaskinportenScope[] =
mapRowElementsToSelectedScopes(updatedRowElements);
const updatedScopes: MaskinportenScopes = { scopes: updatedScopeList };

// TODO: Replace line below with mutation call to update the database
setRowElements(updatedRowElements);
mutateSelectedMaskinportenScopes(updatedScopes);
};

return (
Expand All @@ -92,16 +94,13 @@ export const ScopeList = ({ maskinPortenScopes, selectedScopes }: ScopeListProps
disabled={allScopesDisabled}
/>
<StudioCheckboxTable.Body>
{
// Replace "rowElements" with "checkboxTableRowElements" when ready to implement with mutation function
rowElements.map((rowElement: StudioCheckboxTableRowElement) => (
<ScopeListItem
key={rowElement.value}
rowElement={rowElement}
onChangeScope={handleChangeScope}
/>
))
}
{checkboxTableRowElements.map((rowElement: StudioCheckboxTableRowElement) => (
<ScopeListItem
key={rowElement.value}
rowElement={rowElement}
onChangeScope={handleChangeScope}
/>
))}
</StudioCheckboxTable.Body>
</StudioCheckboxTable>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import '@testing-library/jest-dom';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { renderWithProviders } from 'app-development/test/mocks';
import { type MaskinportenScope } from 'app-shared/types/MaskinportenScope';
import {
type MaskinportenScopes,
type MaskinportenScope,
} from 'app-shared/types/MaskinportenScope';

const scopeMock1: MaskinportenScope = {
scope: 'scope1',
Expand All @@ -18,7 +21,7 @@ const scopeMock2: MaskinportenScope = {
description: 'description2',
};

const maskinportenScopes: MaskinportenScope[] = [scopeMock1, scopeMock2];
const maskinportenScopes: MaskinportenScopes = { scopes: [scopeMock1, scopeMock2] };

describe('ScopeListContainer', () => {
it('should display a spinner while loading', () => {
Expand All @@ -45,7 +48,7 @@ describe('ScopeListContainer', () => {

expect(screen.getAllByRole('checkbox')).toHaveLength(3); // The two scopes + "select all"

maskinportenScopes.forEach((scope: MaskinportenScope) => {
maskinportenScopes.scopes.forEach((scope: MaskinportenScope) => {
expect(screen.getByRole('checkbox', { name: scope.scope }));
expect(screen.getByText(scope.description));
expect(screen.getByRole('checkbox', { name: scope.scope })).not.toBeChecked();
Expand All @@ -69,20 +72,23 @@ describe('ScopeListContainer', () => {

expect(screen.getAllByRole('checkbox')).toHaveLength(3); // The two scopes + "select all"

maskinportenScopes.forEach((scope: MaskinportenScope) => {
maskinportenScopes.scopes.forEach((scope: MaskinportenScope) => {
expect(screen.getByRole('checkbox', { name: scope.scope }));
expect(screen.getByText(scope.description));
expect(screen.getByRole('checkbox', { name: scope.scope })).toBeChecked();
});
});

it('should display a merged list of scopes if both selected scopes and available scopes are available', async () => {
const availableScopes: MaskinportenScopes = { scopes: [scopeMock1] };
const mockGetMaskinportenScopes = jest
.fn()
.mockImplementation(() => Promise.resolve([scopeMock1]));
.mockImplementation(() => Promise.resolve(availableScopes));

const selectedScopes: MaskinportenScopes = { scopes: [scopeMock2] };
const mockGetSelectedMaskinportenScopes = jest
.fn()
.mockImplementation(() => Promise.resolve([scopeMock2]));
.mockImplementation(() => Promise.resolve(selectedScopes));

renderScopeListContainer({
queries: {
Expand All @@ -95,7 +101,7 @@ describe('ScopeListContainer', () => {

expect(screen.getAllByRole('checkbox')).toHaveLength(3); // The two scopes + "select all"

maskinportenScopes.forEach((scope: MaskinportenScope) => {
maskinportenScopes.scopes.forEach((scope: MaskinportenScope) => {
expect(screen.getByRole('checkbox', { name: scope.scope }));
expect(screen.getByText(scope.description));
});
Expand Down
Loading

0 comments on commit bd51ebf

Please sign in to comment.