diff --git a/frontend/dashboard/app/App.tsx b/frontend/dashboard/app/App.tsx index 3339ec1fb15..1211f22104b 100644 --- a/frontend/dashboard/app/App.tsx +++ b/frontend/dashboard/app/App.tsx @@ -11,6 +11,7 @@ import { ErrorMessage } from 'dashboard/components/ErrorMessage'; import './App.css'; import { PageLayout } from 'dashboard/pages/PageLayout'; import { useTranslation } from 'react-i18next'; +import { DASHBOARD_ROOT_ROUTE } from 'app-shared/constants'; export const App = (): JSX.Element => { const { t } = useTranslation(); @@ -51,7 +52,7 @@ export const App = (): JSX.Element => { return (
- }> + }> } diff --git a/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.test.tsx b/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.test.tsx new file mode 100644 index 00000000000..17273bc8d8b --- /dev/null +++ b/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.test.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { render as rtlRender, screen, within } from '@testing-library/react'; +import type { ServiceOwnerSelectorProps } from './ServiceOwnerSelector'; +import { ServiceOwnerSelector } from './ServiceOwnerSelector'; +import { textMock } from '../../../testing/mocks/i18nMock'; +import { user } from 'app-shared/mocks/mocks'; + +const defaultProps = { + selectedOrgOrUser: 'userLogin', + user: { + ...user, + login: 'userLogin', + }, + organizations: [ + { + avatar_url: '', + id: 1, + username: 'organizationUsername', + }, + ], + errorMessage: '', + name: '', +}; + +const render = (props: Partial = {}) => { + rtlRender(); +}; + +describe('ServiceOwnerSelector', () => { + it('renders select with all options', async () => { + render(); + + const select = screen.getByLabelText(textMock('general.service_owner')); + expect( + within(select).getByRole('option', { name: defaultProps.user.login }), + ).toBeInTheDocument(); + expect( + within(select).getByRole('option', { name: defaultProps.organizations[0].username }), + ).toBeInTheDocument(); + }); + + it('shows validation errors', async () => { + const errorMessage = 'Field cannot be empty'; + + render({ errorMessage }); + + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + + it('selects the org when the current context is the org', async () => { + const selectedOrgOrUser = defaultProps.organizations[0].username; + + render({ selectedOrgOrUser }); + + const select = screen.getByLabelText(textMock('general.service_owner')); + expect(select).toHaveValue(selectedOrgOrUser); + }); + + it('selects the user when the current context is the user', async () => { + const selectedOrgOrUser = defaultProps.user.login; + + render({ selectedOrgOrUser }); + + const select = screen.getByLabelText(textMock('general.service_owner')); + expect(select).toHaveValue(selectedOrgOrUser); + }); + + it('selects the user when the current context is invalid', async () => { + const selectedOrgOrUser = 'all'; + + render({ selectedOrgOrUser }); + + const select = screen.getByLabelText(textMock('general.service_owner')); + expect(select).toHaveValue(defaultProps.user.login); + }); +}); diff --git a/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.tsx b/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.tsx index e597a7f936a..a087f162f81 100644 --- a/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.tsx +++ b/frontend/dashboard/components/ServiceOwnerSelector/ServiceOwnerSelector.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { Organization } from 'app-shared/types/Organization'; import type { User } from 'app-shared/types/Repository'; -type ServiceOwnerSelectorProps = { +export type ServiceOwnerSelectorProps = { selectedOrgOrUser: string; user: User; organizations: Organization[]; @@ -26,12 +26,22 @@ export const ServiceOwnerSelector = ({ const selectableOrganizations: SelectableItem[] = mapOrganizationToSelectableItems(organizations); const selectableOptions: SelectableItem[] = [selectableUser, ...selectableOrganizations]; + const defaultValue: string = + selectableOptions.find((item) => item.value === selectedOrgOrUser)?.value ?? + selectableUser.value; + return (
- + {selectableOptions.map(({ value, label }) => (
diff --git a/frontend/dashboard/pages/PageLayout/PageLayout.test.tsx b/frontend/dashboard/pages/PageLayout/PageLayout.test.tsx new file mode 100644 index 00000000000..0592dfb6435 --- /dev/null +++ b/frontend/dashboard/pages/PageLayout/PageLayout.test.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { MockServicesContextWrapper } from '../../dashboardTestUtils'; +import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext'; +import { organization, user } from 'app-shared/mocks/mocks'; +import { PageLayout } from './PageLayout'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { DASHBOARD_ROOT_ROUTE } from 'app-shared/constants'; +import { useParams } from 'react-router-dom'; +import { SelectedContextType } from 'app-shared/navigation/main-header/Header'; + +const mockedNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockedNavigate, + useParams: jest.fn(), +})); + +const renderWithMockServices = (services?: Partial) => { + const queryClient = createQueryClientMock(); + queryClient.setQueryData( + [QueryKey.Organizations], + [ + { + ...organization, + username: 'ttd', + }, + ], + ); + queryClient.setQueryData([QueryKey.CurrentUser], user); + + render( + + + , + ); +}; + +describe('PageLayout', () => { + test('should not redirect to root if context is self', async () => { + (useParams as jest.Mock).mockReturnValue({ + selectedContext: SelectedContextType.Self, + }); + renderWithMockServices(); + expect(mockedNavigate).not.toHaveBeenCalled(); + }); + + test('should not redirect to root if context is all', async () => { + (useParams as jest.Mock).mockReturnValue({ + selectedContext: SelectedContextType.All, + }); + renderWithMockServices(); + expect(mockedNavigate).not.toHaveBeenCalled(); + }); + + test('should not redirect to root if user have access to selected context', async () => { + (useParams as jest.Mock).mockReturnValue({ + selectedContext: 'ttd', + }); + renderWithMockServices(); + expect(mockedNavigate).not.toHaveBeenCalled(); + }); + + test('should redirect to root if user does not have access to selected context', async () => { + (useParams as jest.Mock).mockReturnValue({ + selectedContext: 'test', + }); + renderWithMockServices(); + expect(mockedNavigate).toHaveBeenCalledTimes(1); + expect(mockedNavigate).toHaveBeenCalledWith(DASHBOARD_ROOT_ROUTE); + }); +}); diff --git a/frontend/dashboard/pages/PageLayout/PageLayout.tsx b/frontend/dashboard/pages/PageLayout/PageLayout.tsx index 1601a8e0362..86fc643a37b 100644 --- a/frontend/dashboard/pages/PageLayout/PageLayout.tsx +++ b/frontend/dashboard/pages/PageLayout/PageLayout.tsx @@ -7,6 +7,7 @@ import type { IHeaderContext } from 'app-shared/navigation/main-header/Header'; import { userHasAccessToSelectedContext } from '../../utils/userUtils'; import { useSelectedContext } from 'dashboard/hooks/useSelectedContext'; +import { DASHBOARD_ROOT_ROUTE } from 'app-shared/constants'; export const PageLayout = () => { const { data: user } = useUserQuery(); @@ -20,7 +21,7 @@ export const PageLayout = () => { organizations && !userHasAccessToSelectedContext({ selectedContext, orgs: organizations }) ) { - navigate('/'); + navigate(DASHBOARD_ROOT_ROUTE); } }, [organizations, selectedContext, user.login, navigate]); diff --git a/frontend/packages/shared/src/constants.js b/frontend/packages/shared/src/constants.js index 05288400180..a5725a3317f 100644 --- a/frontend/packages/shared/src/constants.js +++ b/frontend/packages/shared/src/constants.js @@ -1,5 +1,7 @@ +// TODO: Extract/Centralize react-router routes (https://github.com/Altinn/altinn-studio/issues/12624) export const APP_DEVELOPMENT_BASENAME = '/editor'; export const DASHBOARD_BASENAME = '/dashboard'; +export const DASHBOARD_ROOT_ROUTE = '/'; export const RESOURCEADM_BASENAME = '/resourceadm'; export const PREVIEW_BASENAME = '/preview'; export const STUDIO_ROOT_BASENAME = '/';