diff --git a/.github/workflows/app-react.yml b/.github/workflows/app-react.yml index 3ef4c2f3ea..96bd8d1da5 100644 --- a/.github/workflows/app-react.yml +++ b/.github/workflows/app-react.yml @@ -54,6 +54,8 @@ jobs: - run: npm run coverage -- --maxWorkers=2 working-directory: ${{env.working-directory}} + env: + REACT_APP_TENANT: MOTI - name: Codecov uses: codecov/codecov-action@v3.1.1 diff --git a/source/frontend/src/AppRouter.test.tsx b/source/frontend/src/AppRouter.test.tsx new file mode 100644 index 0000000000..dc652e8eb5 --- /dev/null +++ b/source/frontend/src/AppRouter.test.tsx @@ -0,0 +1,262 @@ +import axios from 'axios'; +import { createMemoryHistory } from 'history'; + +import AppRouter from './AppRouter'; +import { Claims } from './constants'; +import { ADD_ACTIVATE_USER, GET_REQUEST_ACCESS } from './constants/actionTypes'; +import { AuthStateContext } from './contexts/authStateContext'; +import { IGeocoderResponse } from './hooks/pims-api/interfaces/IGeocoder'; +import { useApiAcquisitionFile } from './hooks/pims-api/useApiAcquisitionFile'; +import { useApiGeocoder } from './hooks/pims-api/useApiGeocoder'; +import { useApiLeases } from './hooks/pims-api/useApiLeases'; +import { useApiProperties } from './hooks/pims-api/useApiProperties'; +import { useApiResearchFile } from './hooks/pims-api/useApiResearchFile'; +import { useApiUsers } from './hooks/pims-api/useApiUsers'; +import { ILeaseSearchResult, IPagedItems, IProperty } from './interfaces'; +import { IResearchSearchResult } from './interfaces/IResearchSearchResult'; +import { mockLookups } from './mocks/lookups.mock'; +import { getMockPagedUsers, getUserMock } from './mocks/user.mock'; +import { Api_AcquisitionFile } from './models/api/AcquisitionFile'; +import { lookupCodesSlice } from './store/slices/lookupCodes'; +import { networkSlice } from './store/slices/network/networkSlice'; +import { tenantsSlice } from './store/slices/tenants'; +import { defaultTenant } from './tenants/config/defaultTenant'; +import { mockKeycloak, render, RenderOptions, screen } from './utils/test-utils'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +const history = createMemoryHistory(); +const storeState = { + [tenantsSlice.name]: { defaultTenant }, + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, + [networkSlice.name]: { + [ADD_ACTIVATE_USER]: {}, + [GET_REQUEST_ACCESS]: { + isFetching: false, + }, + }, + loadingBar: {}, + keycloakReady: true, +}; + +jest.mock('@react-keycloak/web'); + +// Mock React.Suspense in tests +jest.mock('react', () => { + const React = jest.requireActual('react'); + React.Suspense = ({ children }: any) => children; + return React; +}); + +// Need to mock this library for unit tests +jest.mock('react-visibility-sensor', () => { + return jest.fn().mockImplementation(({ children }) => { + if (children instanceof Function) { + return children({ isVisible: true }); + } + return children; + }); +}); + +jest.mock('@/hooks/usePimsIdleTimer'); + +jest.mock('@/hooks/pims-api/useApiHealth', () => ({ + useApiHealth: () => ({ + getVersion: jest.fn().mockResolvedValue({ data: { environment: 'test', version: '1.0.0.0' } }), + }), +})); + +jest.mock('@/store/slices/tenants/useTenants', () => ({ + useTenants: () => ({ getSettings: jest.fn() }), +})); + +jest.mock('./hooks/pims-api/useApiUsers'); +(useApiUsers as jest.MockedFunction).mockReturnValue({ + activateUser: jest.fn(), + getUser: jest.fn().mockResolvedValue({ data: getUserMock() }), + getUserInfo: jest.fn().mockResolvedValue({ data: getUserMock() }), + getUsersPaged: jest.fn().mockResolvedValue({ data: getMockPagedUsers() }), + putUser: jest.fn(), + exportUsers: jest.fn(), +}); + +jest.mock('./hooks/pims-api/useApiProperties'); +(useApiProperties as jest.MockedFunction).mockReturnValue({ + getPropertiesPagedApi: jest.fn().mockResolvedValue({ data: {} as IPagedItems }), + getMatchingPropertiesApi: jest.fn(), + getPropertyAssociationsApi: jest.fn(), + exportPropertiesApi: jest.fn(), + getPropertiesApi: jest.fn(), + getPropertyConceptWithIdApi: jest.fn(), + putPropertyConceptApi: jest.fn(), +}); + +jest.mock('./hooks/pims-api/useApiLeases'); +(useApiLeases as jest.MockedFunction).mockReturnValue({ + getLeases: jest.fn().mockResolvedValue({ data: {} as IPagedItems }), + getApiLease: jest.fn(), + getLastUpdatedByApi: jest.fn(), + postLease: jest.fn(), + putApiLease: jest.fn(), + exportLeases: jest.fn(), + exportAggregatedLeases: jest.fn(), + exportLeasePayments: jest.fn(), +}); + +jest.mock('./hooks/pims-api/useApiAcquisitionFile'); +(useApiAcquisitionFile as jest.MockedFunction).mockReturnValue({ + getAcquisitionFiles: jest + .fn() + .mockResolvedValue({ data: {} as IPagedItems }), + getAcquisitionFile: jest.fn(), + getLastUpdatedByApi: jest.fn(), + getAgreementReport: jest.fn(), + getCompensationReport: jest.fn(), + exportAcquisitionFiles: jest.fn(), + postAcquisitionFile: jest.fn(), + putAcquisitionFile: jest.fn(), + putAcquisitionFileProperties: jest.fn(), + getAcquisitionFileProperties: jest.fn(), + getAcquisitionFileOwners: jest.fn(), + getAllAcquisitionFileTeamMembers: jest.fn(), + getAcquisitionFileProject: jest.fn(), + getAcquisitionFileProduct: jest.fn(), + getAcquisitionFileChecklist: jest.fn(), + putAcquisitionFileChecklist: jest.fn(), + getFileCompensationRequisitions: jest.fn(), + getFileCompReqH120s: jest.fn(), + postFileCompensationRequisition: jest.fn(), + getAcquisitionFileForm8s: jest.fn(), + postFileForm8: jest.fn(), +}); + +jest.mock('./hooks/pims-api/useApiResearchFile'); +(useApiResearchFile as jest.MockedFunction).mockReturnValue({ + getResearchFiles: jest.fn().mockResolvedValue({ data: {} as IPagedItems }), + getResearchFile: jest.fn(), + postResearchFile: jest.fn(), + putResearchFile: jest.fn(), + getLastUpdatedByApi: jest.fn(), + putResearchFileProperties: jest.fn(), + putPropertyResearchFile: jest.fn(), + getResearchFileProperties: jest.fn(), +}); + +jest.mock('./hooks/pims-api/useApiGeocoder'); +(useApiGeocoder as jest.MockedFunction).mockReturnValue({ + searchAddressApi: jest.fn().mockResolvedValue({ data: [] as IGeocoderResponse[] }), + getSitePidsApi: jest.fn(), + getNearestToPointApi: jest.fn(), +}); + +describe('PSP routing', () => { + const setup = (url: string = '/', renderOptions: RenderOptions = {}) => { + history.replace(url); + const utils = render( + + + , + { + ...renderOptions, + store: storeState, + history, + }, + ); + + return { ...utils }; + }; + + beforeEach(() => { + mockedAxios.get.mockResolvedValue({ data: {}, status: 200 }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('public routes', () => { + beforeEach(() => { + mockKeycloak({ authenticated: false }); + }); + + it('should redirect unauthenticated user to the login page', async () => { + const { getByText } = setup('/'); + expect(getByText('Sign into PIMS with your government issued IDIR')).toBeVisible(); + }); + + it('should show header and footer links', async () => { + const { getByRole } = setup('/'); + expect(getByRole('link', { name: 'Disclaimer' })).toHaveAttribute( + 'href', + 'http://www.gov.bc.ca/gov/content/home/disclaimer', + ); + }); + + it('should show a page for non-supported browsers', async () => { + const { getByText } = setup('/ienotsupported'); + expect( + getByText('Please use a supported internet browser such as Chrome, Firefox or Edge.'), + ).toBeVisible(); + }); + + it('should show the access denied page', async () => { + const { getByText, getByRole } = setup('/forbidden'); + expect(getByText('You do not have permission to view this page')).toBeVisible(); + expect(getByRole('link', { name: 'Go back to the map' })).toBeVisible(); + }); + + it.each(['/page-not-found', '/fake-url'])( + 'should show the not found page when route is %s', + async url => { + const { getByText, getByRole } = setup(url); + expect(getByText('Page not found')).toBeVisible(); + expect(getByRole('link', { name: 'Go back to the map' })).toBeVisible(); + }, + ); + }); + + describe('authenticated routes', () => { + it('should display the property list view', async () => { + setup('/properties/list', { claims: [Claims.PROPERTY_VIEW] }); + const lazyElement = await screen.findByText('Civic Address'); + expect(lazyElement).toBeInTheDocument(); + expect(document.title).toMatch(/View Inventory/i); + }); + + it('should display the lease list view', async () => { + setup('/lease/list', { claims: [Claims.LEASE_VIEW] }); + const lazyElement = await screen.findByText('L-File Number'); + expect(lazyElement).toBeInTheDocument(); + expect(document.title).toMatch(/View Lease & Licenses/i); + }); + + it('should display the acquisition list view', async () => { + setup('/acquisition/list', { claims: [Claims.ACQUISITION_VIEW] }); + const lazyElement = await screen.findByText('Acquisition file name'); + expect(lazyElement).toBeInTheDocument(); + expect(document.title).toMatch(/View Acquisition Files/i); + }); + + it('should display the research list view', async () => { + setup('/research/list', { claims: [Claims.RESEARCH_VIEW] }); + const lazyElement = await screen.findByText('File #'); + expect(lazyElement).toBeInTheDocument(); + expect(document.title).toMatch(/View Research Files/i); + }); + + it('should display the admin users page at the expected route', async () => { + setup('/admin/users', { claims: [Claims.ADMIN_USERS] }); + const lazyElement = await screen.findByText('User Management'); + expect(lazyElement).toBeInTheDocument(); + expect(document.title).toMatch(/Users Management/i); + }); + + it('should display the edit user page at the expected route', async () => { + setup('/admin/user/1', { claims: [Claims.ADMIN_USERS] }); + const lazyElement = await screen.findByText('User Information'); + expect(lazyElement).toBeInTheDocument(); + expect(document.title).toMatch(/Edit User/i); + }); + }); +}); diff --git a/source/frontend/src/components/common/form/LimitedSelect.test.tsx b/source/frontend/src/components/common/form/LimitedSelect.test.tsx deleted file mode 100644 index 541042c1af..0000000000 --- a/source/frontend/src/components/common/form/LimitedSelect.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// import React from 'react'; - -// import { PropertyClassificationTypes } from '@/constants/propertyClassificationTypes'; -// import { getIn, useFormikContext } from 'formik'; -// import renderer from 'react-test-renderer'; - -// import { FastSelect } from './FastSelect'; - -// jest.mock('formik'); - -// (useFormikContext as jest.Mock).mockReturnValue({ -// values: { -// classificationId: PropertyClassificationTypes.CoreOperational, -// classification: 'zero', -// }, -// registerField: jest.fn(), -// unregisterField: jest.fn(), -// }); -// (getIn as jest.Mock).mockReturnValue(0); - -// const options = [ -// { -// label: 'zero', -// value: '0', -// selected: true, -// }, -// { -// label: 'one', -// value: '1', -// selected: true, -// }, -// { -// label: 'two', -// value: '2', -// selected: true, -// }, -// ]; - -describe('LimitedSelect - Enzyme Tests - NEEDS REFACTORING', () => { - it('should be implemented', async () => {}); - // it('limited fast select renders correctly', () => { - // const context = useFormikContext(); - // const tree = renderer - // .create( - // , - // ) - // .toJSON(); - // expect(tree).toMatchSnapshot(); - // }); - // - // it('only renders the limited options + the previous value', async () => { - // const context = useFormikContext(); - // const component = mount( - // , - // ); - // expect(component.find('option')).toHaveLength(2); - // }); -}); - -// TODO: Remove this line when unit tests above are fixed -export {}; diff --git a/source/frontend/src/components/common/form/PrimaryContactSelector/PrimaryContactSelector.tsx b/source/frontend/src/components/common/form/PrimaryContactSelector/PrimaryContactSelector.tsx index 4268a69899..7419221cae 100644 --- a/source/frontend/src/components/common/form/PrimaryContactSelector/PrimaryContactSelector.tsx +++ b/source/frontend/src/components/common/form/PrimaryContactSelector/PrimaryContactSelector.tsx @@ -53,7 +53,7 @@ export const PrimaryContactSelector: React.FC = ({ ) : primaryContacts.length > 0 ? ( {primaryContacts[0].label} ) : ( - 'No contacts available' + No contacts available ); } diff --git a/source/frontend/src/components/common/form/Select.test.tsx b/source/frontend/src/components/common/form/Select.test.tsx new file mode 100644 index 0000000000..a336fc011d --- /dev/null +++ b/source/frontend/src/components/common/form/Select.test.tsx @@ -0,0 +1,154 @@ +import { Formik, FormikProps } from 'formik'; +import React from 'react'; + +import { act, fireEvent, render, RenderOptions, screen, userEvent } from '@/utils/test-utils'; + +import { Select, SelectOption, SelectProps } from './Select'; + +const countries: SelectOption[] = [ + { label: 'Austria', value: 'AT' }, + { label: 'United States', value: 'US' }, + { label: 'Ireland', value: 'IE' }, +]; + +const onSubmit = jest.fn(); + +describe('Select component', () => { + const setup = ( + options: RenderOptions & { props?: Partial } & { + initialValues?: { countryId: string }; + } = {}, + ) => { + const formikRef = React.createRef>(); + const utils = render( + + + + + + + + + +`; diff --git a/source/frontend/src/mocks/accessRequest.mock.ts b/source/frontend/src/mocks/accessRequest.mock.ts index 6b06ba8755..c51a2f88af 100644 --- a/source/frontend/src/mocks/accessRequest.mock.ts +++ b/source/frontend/src/mocks/accessRequest.mock.ts @@ -1,3 +1,4 @@ +import { IPagedItems } from '@/interfaces'; import { Api_AccessRequest } from '@/models/api/AccessRequest'; export const getMockAccessRequest = (): Api_AccessRequest => ({ @@ -76,7 +77,7 @@ export const getMockAccessRequest = (): Api_AccessRequest => ({ rowVersion: 1, }); -export const getMockPagedAccessRequests = () => ({ +export const getMockPagedAccessRequests = (): IPagedItems => ({ items: [ { id: 7, @@ -238,6 +239,7 @@ export const getMockPagedAccessRequests = () => ({ rowVersion: 4, }, ], + pageIndex: 0, page: 1, quantity: 10, total: 2, diff --git a/source/frontend/src/mocks/user.mock.ts b/source/frontend/src/mocks/user.mock.ts index 179b30ec79..e2634a4f58 100644 --- a/source/frontend/src/mocks/user.mock.ts +++ b/source/frontend/src/mocks/user.mock.ts @@ -1,3 +1,4 @@ +import { IPagedItems } from '@/interfaces'; import { Api_User } from '@/models/api/User'; export const getUserMock = (): Api_User => ({ @@ -145,7 +146,7 @@ export const getUserMock = (): Api_User => ({ rowVersion: 107, }); -export const getMockPagedUsers = () => ({ +export const getMockPagedUsers = (): IPagedItems => ({ items: [ { id: 30, @@ -538,6 +539,7 @@ export const getMockPagedUsers = () => ({ }, ], page: 1, + pageIndex: 0, quantity: 5, total: 42, }); diff --git a/source/frontend/src/router.test.tsx b/source/frontend/src/router.test.tsx deleted file mode 100644 index 588ffbb503..0000000000 --- a/source/frontend/src/router.test.tsx +++ /dev/null @@ -1,190 +0,0 @@ -// import { waitFor } from '@testing-library/react'; -// import AppRouter from '@/AppRouter'; -// import axios from 'axios'; -// import MockAdapter from 'axios-mock-adapter'; -// import { Footer, Header } from '@/components/layout'; -// import Map from '@/components/maps/leaflet/Map'; -// import { Claims } from '@/constants/index'; -// import { AuthStateContextProvider } from '@/contexts/authStateContext'; -// import { IENotSupportedPage } from '@/features/account/IENotSupportedPage'; -// import Login from '@/features/account/Login'; -// import ManageAccessRequestsPage from '@/features/admin/access/ManageAccessRequestsPage'; -// import AccessRequestPage from '@/features/admin/access-request/AccessRequestPage'; -// import EditUserPage from '@/features/admin/edit-user/EditUserPage'; -// import ManageUsers from '@/features/admin/users/ManageUsersPage'; -// import { PropertyListView } from '@/features/properties/list'; -// import { Formik } from 'formik'; -// import { createMemoryHistory } from 'history'; -// import { enableFetchMocks } from 'jest-fetch-mock'; -// import { noop } from 'lodash'; -// import AccessDenied from '@/pages/401/AccessDenied'; -// import { NotFoundPage } from '@/pages/404/NotFoundPage'; -// import Test from '@/pages/Test.ignore'; -// import { act } from 'react-dom/test-utils'; -// import { flushPromises, mockKeycloak } from '@/utils/test-utils'; -// import TestCommonWrapper from '@/utils/TestCommonWrapper'; - -// const mockAxios = new MockAdapter(axios); - -// enableFetchMocks(); -// jest.mock('@react-keycloak/web'); -// const history = createMemoryHistory(); - -describe('PSP routing - Enzyme Tests - NEEDS REFACTORING', () => { - it('should be implemented', async () => {}); - - // beforeEach(() => { - // fetchMock.mockResponse(JSON.stringify({ status: 200, body: {} })); - // }); - // const getRouter = (url: string) => { - // history.push(url); - // return ( - // - // - // - // - // - // - // - // ); - // }; - // describe('unauth routes', () => { - // let wrapper: any; - // beforeEach(() => { - // mockKeycloak({ authenticated: false }); - // mockAxios.onAny().reply(200, {}); - // }); - // afterEach(() => { - // wrapper.unmount(); - // }); - // it('valid path should redirect unauthenticated user to the login page', async () => { - // wrapper = mount(getRouter('/')); - // await act(async () => { - // await flushPromises(); - // }); - // expect(wrapper.find(Login)).toHaveLength(1); - // }); - // it('unauthenticated users should see the Header', async () => { - // wrapper = mount(getRouter('/')); - // await act(async () => { - // await flushPromises(); - // }); - // expect(wrapper.find(Header)).toHaveLength(1); - // }); - // it('unauthenticated users should see the footer', async () => { - // wrapper = mount(getRouter('/')); - // await act(async () => { - // await flushPromises(); - // }); - // expect(wrapper.find(Footer)).toHaveLength(1); - // }); - // it('displays the ie warning page at the expected route', async () => { - // wrapper = mount(getRouter('/ienotsupported')); - // await act(async () => { - // await flushPromises(); - // }); - // expect(wrapper.find(IENotSupportedPage)).toHaveLength(1); - // }); - // it('displays the forbidden page at the expected route', async () => { - // wrapper = mount(getRouter('/forbidden')); - // await act(async () => { - // await flushPromises(); - // }); - // expect(wrapper.find(AccessDenied)).toHaveLength(1); - // }); - // it('displays not found page at the expected route', async () => { - // wrapper = mount(getRouter('/page-not-found')); - // await act(async () => { - // await flushPromises(); - // }); - // expect(wrapper.find(NotFoundPage)).toHaveLength(1); - // }); - // it('displays not found page at an unknown route', async () => { - // wrapper = mount(getRouter('/fake')); - // await act(async () => { - // await flushPromises(); - // }); - // expect(wrapper.find(NotFoundPage)).toHaveLength(1); - // }); - // it('displays the test page at the expected route', async () => { - // wrapper = mount(getRouter('/test')); - // await act(async () => { - // await flushPromises(); - // }); - // expect(wrapper.find(Test)).toHaveLength(1); - // }); - // }); - // describe('auth routes', () => { - // beforeEach(() => { - // mockKeycloak({ - // claims: [Claims.PROPERTY_VIEW, Claims.ADMIN_USERS], - // roles: [Claims.PROPERTY_VIEW], - // authenticated: true, - // }); - // mockAxios.onAny().reply(200, {}); - // delete (window as any).ResizeObserver; - // window.ResizeObserver = jest.fn().mockImplementation(() => ({ - // observe: jest.fn(), - // unobserve: jest.fn(), - // disconnect: jest.fn(), - // })); - // }); - // it('displays the mapview on the home page', async () => { - // const wrapper = mount(getRouter('/mapView')); - // await waitFor(async () => { - // wrapper.update(); - // expect(wrapper.find(Map)).toHaveLength(1); - // }); - // }); - // it('displays the admin users page at the expected route', async () => { - // const wrapper = mount(getRouter('/admin/users')); - // await waitFor(async () => { - // wrapper.update(); - // expect(wrapper.find(ManageUsers)).toHaveLength(1); - // }); - // }); - // it('displays the admin access requests page at the expected route', async () => { - // const wrapper = mount(getRouter('/admin/access/requests')); - // await waitFor(async () => { - // wrapper.update(); - // expect(wrapper.find(ManageAccessRequestsPage)).toHaveLength(1); - // }); - // }); - // it('displays the access request page at the expected route', async () => { - // const wrapper = mount(getRouter('/access/request')); - // await waitFor(async () => { - // wrapper.update(); - // expect(wrapper.find(AccessRequestPage)).toHaveLength(1); - // }); - // }); - // it('displays the property list view at the expected route', async () => { - // const wrapper = mount(getRouter('/properties/list')); - // await waitFor(async () => { - // wrapper.update(); - // expect(wrapper.find(PropertyListView)).toHaveLength(1); - // }); - // }); - // it('displays the edit user page at the expected route', async () => { - // const wrapper = mount(getRouter('/admin/user/1')); - // await waitFor(async () => { - // wrapper.update(); - // expect(wrapper.find(EditUserPage)).toHaveLength(1); - // }); - // }); - // }); -}); - -// TODO: Remove this line when unit tests above are fixed -export {};