From 1bbcd8b8feca1b05f6c452b5ef710f0328e2a958 Mon Sep 17 00:00:00 2001 From: Quentin Ruhier Date: Fri, 17 Jan 2025 11:21:32 +0100 Subject: [PATCH] test: add unit tests on pages --- src/ui/pages/Error/Error.test.tsx | 51 +++++++ src/ui/pages/External/External.test.tsx | 52 +++++++ src/ui/pages/collect/Collect.test.tsx | 62 +++++++++ src/ui/pages/review/Review.test.tsx | 131 ++++++++++++++++++ .../pages/synchronize/LoadingDisplay.test.tsx | 99 +++++++++++++ src/ui/pages/visualize/Visualize.test.tsx | 96 +++++++++++++ src/ui/pages/visualize/Visualize.tsx | 3 +- src/ui/pages/visualize/VisualizeForm.test.tsx | 100 +++++++++++++ .../pages/visualize/getSearchParams.test.ts | 95 +++++++++++++ 9 files changed, 688 insertions(+), 1 deletion(-) create mode 100644 src/ui/pages/Error/Error.test.tsx create mode 100644 src/ui/pages/External/External.test.tsx create mode 100644 src/ui/pages/collect/Collect.test.tsx create mode 100644 src/ui/pages/review/Review.test.tsx create mode 100644 src/ui/pages/synchronize/LoadingDisplay.test.tsx create mode 100644 src/ui/pages/visualize/Visualize.test.tsx create mode 100644 src/ui/pages/visualize/VisualizeForm.test.tsx create mode 100644 src/ui/pages/visualize/getSearchParams.test.ts diff --git a/src/ui/pages/Error/Error.test.tsx b/src/ui/pages/Error/Error.test.tsx new file mode 100644 index 00000000..c3d8743e --- /dev/null +++ b/src/ui/pages/Error/Error.test.tsx @@ -0,0 +1,51 @@ +import { render } from '@testing-library/react' +import { isRouteErrorResponse, useRouteError } from 'react-router-dom' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { ErrorComponent } from '@/ui/components/ErrorComponent' + +import { ErrorPage } from './Error' + +vi.mock('react-router-dom', () => ({ + useRouteError: vi.fn(), + isRouteErrorResponse: vi.fn(), +})) +vi.mock('@/i18n', () => ({ + useTranslation: () => ({ t: (keyMessage: string) => keyMessage }), +})) +vi.mock('@/ui/components/ErrorComponent', () => ({ + ErrorComponent: vi.fn(), +})) + +afterEach(() => { + vi.clearAllMocks() +}) + +describe('ErrorPage', () => { + it('calls ErrorComponent with the correct message for instance of Error', () => { + const mockError = new Error('Something went wrong') + vi.mocked(useRouteError).mockReturnValue(mockError) + + render() + + expect(ErrorComponent).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Something went wrong' }), + {}, + ) + }) + + it('renders ErrorComponent with the correct message for RouteErrorResponse', () => { + const mockRouteError = { status: 404, statusText: 'Not Found' } + vi.mocked(useRouteError).mockReturnValue(mockRouteError) + vi.mocked(isRouteErrorResponse).mockReturnValue(true) + + render() + + expect(ErrorComponent).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'error 404 : Not Found', + }), + {}, + ) + }) +}) diff --git a/src/ui/pages/External/External.test.tsx b/src/ui/pages/External/External.test.tsx new file mode 100644 index 00000000..bb67f715 --- /dev/null +++ b/src/ui/pages/External/External.test.tsx @@ -0,0 +1,52 @@ +import { render } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' + +import { CenteredSpinner } from '@/ui/components/CenteredSpinner' +import { ErrorComponent } from '@/ui/components/ErrorComponent' + +import { ExternalRessources } from './External' +import useScript from './useScript' + +vi.mock('@/i18n', () => ({ + getTranslation: () => ({ t: (keyMessage: string) => keyMessage }), +})) +vi.mock('@/ui/components/ErrorComponent', () => ({ + ErrorComponent: vi.fn(), +})) +vi.mock('@/ui/components/CenteredSpinner', () => ({ + CenteredSpinner: vi.fn(), +})) +vi.mock('./useScript', () => ({ + default: vi.fn(), +})) + +describe('ExternalRessources', () => { + it('renders CenteredSpinner when status is "loading"', () => { + vi.mocked(useScript).mockReturnValue('loading') + + render() + + expect(CenteredSpinner).toHaveBeenCalled() + }) + + it('renders when status is "ready"', () => { + vi.mocked(useScript).mockReturnValue('ready') + + const { container } = render() + + // Check that is rendered in the container + const capmiAppElement = container.querySelector('capmi-app') + expect(capmiAppElement).toBeInTheDocument() + }) + + it('renders ErrorComponent with correct message when status is "error"', () => { + vi.mocked(useScript).mockReturnValue('error') + + render() + + expect(ErrorComponent).toHaveBeenCalledWith( + expect.objectContaining({ message: 'externalResourcesLoadedError' }), + {}, + ) + }) +}) diff --git a/src/ui/pages/collect/Collect.test.tsx b/src/ui/pages/collect/Collect.test.tsx new file mode 100644 index 00000000..b70aa41a --- /dev/null +++ b/src/ui/pages/collect/Collect.test.tsx @@ -0,0 +1,62 @@ +import { render } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' + +import { useCore } from '@/core' +import { Orchestrator } from '@/ui/components/orchestrator/Orchestrator' +import { useLoaderData } from '@/ui/routing/utils' + +import { Collect } from './Collect' + +vi.mock('@/core', () => ({ + useCore: vi.fn(), +})) +vi.mock('@/ui/routing/utils', () => ({ + useLoaderData: vi.fn(), +})) +vi.mock('@/ui/components/orchestrator/Orchestrator', () => ({ + Orchestrator: vi.fn(), +})) + +describe('Collect Component', () => { + it('renders Orchestrator with the correct props', () => { + const mockLoaderData = { + questionnaire: { id: 'q1', title: 'Questionnaire 1' }, + surveyUnit: { id: 'su1', name: 'Survey Unit 1' }, + } + + vi.mocked(useLoaderData).mockReturnValue(mockLoaderData) + + const mockCollectSurvey = { + getReferentiel: vi.fn(), + changePage: vi.fn(), + changeSurveyUnitState: vi.fn(), + quit: vi.fn(), + retrieveQuestionnaireId: vi.fn(), + loader: vi.fn(), + } + + const mockCore = { + functions: { + collectSurvey: mockCollectSurvey, + }, + } + + vi.mocked(useCore).mockReturnValue(mockCore as any) + + render() + + expect(Orchestrator).toHaveBeenCalledWith( + expect.objectContaining({ + source: mockLoaderData.questionnaire, + surveyUnit: mockLoaderData.surveyUnit, + readonly: false, + onQuit: mockCollectSurvey.quit, + onDefinitiveQuit: mockCollectSurvey.quit, + onChangePage: mockCollectSurvey.changePage, + getReferentiel: mockCollectSurvey.getReferentiel, + onChangeSurveyUnitState: mockCollectSurvey.changeSurveyUnitState, + }), + {}, + ) + }) +}) diff --git a/src/ui/pages/review/Review.test.tsx b/src/ui/pages/review/Review.test.tsx new file mode 100644 index 00000000..694b4631 --- /dev/null +++ b/src/ui/pages/review/Review.test.tsx @@ -0,0 +1,131 @@ +import { render } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { useCore } from '@/core' +import type { SurveyUnit } from '@/core/model' +import { Modal } from '@/ui/components/Modal' +import { Orchestrator } from '@/ui/components/orchestrator/Orchestrator' +import { useLoaderData } from '@/ui/routing/utils' + +import { Review } from './Review' + +vi.mock('@/ui/routing/utils', () => ({ + useLoaderData: vi.fn(), +})) + +vi.mock('@/core', () => ({ + useCore: vi.fn(), +})) + +vi.mock('@/i18n', () => ({ + useTranslation: () => ({ t: (keyMessage: string) => keyMessage }), +})) + +vi.mock('@/ui/components/Modal', () => ({ + Modal: vi.fn(), +})) + +vi.mock('@/ui/components/orchestrator/Orchestrator', () => ({ + Orchestrator: vi.fn(), +})) + +describe('Review', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('renders Orchestrator with correct props', () => { + const mockLoaderData = { + questionnaire: { id: 'q1' }, + surveyUnit: { id: 'su1' }, + } + + vi.mocked(useLoaderData).mockReturnValue(mockLoaderData) + + const mockReviewSurvey = { + getReferentiel: vi.fn(), + } + + const mockCore = { + functions: { + reviewSurvey: mockReviewSurvey, + }, + } + + vi.mocked(useCore).mockReturnValue(mockCore as any) + + render() + + expect(Orchestrator).toHaveBeenCalledWith( + expect.objectContaining({ + source: mockLoaderData.questionnaire, + surveyUnit: mockLoaderData.surveyUnit, + readonly: true, + onQuit: expect.any(Function), + onDefinitiveQuit: expect.any(Function), + onChangePage: undefined, + getReferentiel: mockReviewSurvey.getReferentiel, + }), + {}, + ) + }) + + it('opens and closes the quit modal', () => { + const mockLoaderData = { + questionnaire: { id: 'q1' }, + surveyUnit: { id: 'su1' }, + } + + vi.mocked(useLoaderData).mockReturnValue(mockLoaderData) + + const mockReviewSurvey = { + getReferentiel: vi.fn(), + } + + const mockCore = { + functions: { + reviewSurvey: mockReviewSurvey, + }, + } + + vi.mocked(useCore).mockReturnValue(mockCore as any) + + const { rerender } = render() + + // Simulate Orchestrator's onQuit call + const { onQuit } = vi.mocked(Orchestrator).mock.calls[0][0] + + if (onQuit) { + onQuit(mockLoaderData.surveyUnit as SurveyUnit) + } + + rerender() + + expect(Modal).toHaveBeenCalledWith( + expect.objectContaining({ + isOpen: true, + dialogTitle: 'reviewQuitTitle', + dialogContent: 'reviewQuitContent', + buttons: expect.arrayContaining([ + expect.objectContaining({ label: 'cancel', autoFocus: false }), + ]), + onClose: expect.any(Function), + }), + expect.anything(), + ) + + // Simulate Modal's onClose call + const modalProps = vi.mocked(Modal).mock.calls[0][0] + modalProps.onClose() + + // Rerender to reflect state change + rerender() + + expect(Modal).toHaveBeenLastCalledWith( + expect.objectContaining({ + isOpen: false, + }), + expect.anything(), + ) + }) +}) diff --git a/src/ui/pages/synchronize/LoadingDisplay.test.tsx b/src/ui/pages/synchronize/LoadingDisplay.test.tsx new file mode 100644 index 00000000..78373370 --- /dev/null +++ b/src/ui/pages/synchronize/LoadingDisplay.test.tsx @@ -0,0 +1,99 @@ +import LinearProgress from '@mui/material/LinearProgress' +import { render } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { LoadingDisplay } from './LoadingDisplay' + +vi.mock('@/i18n', () => ({ + useTranslation: () => ({ t: (keyMessage: string) => keyMessage }), +})) + +vi.mock('@mui/material/LinearProgress', () => ({ + __esModule: true, + default: vi.fn(), +})) + +afterEach(() => { + vi.clearAllMocks() +}) + +describe('LoadingDisplay Component', () => { + it('renders synchronization title and step title', () => { + const props = { + syncStepTitle: 'sync step', + progressBars: [{ label: 'Progress 1', progress: 50 }], + } + + const { getByText } = render() + + expect(getByText('synchronizationInProgress')).toBeInTheDocument() + expect(getByText('sync step')).toBeInTheDocument() + }) + + it('renders progress bars with labels', () => { + const props = { + syncStepTitle: 'sync step', + progressBars: [ + { label: 'Progress 1', progress: 50 }, + { label: 'Progress 2', progress: 75, extraTitle: 'Extra Info' }, + ], + } + + const { getByText } = render() + + // Check progress labels + expect(getByText('Progress 1')).toBeInTheDocument() + expect(getByText('Progress 2: Extra Info')).toBeInTheDocument() + + // Check progress bars + expect(LinearProgress).toHaveBeenCalledTimes(2) + expect(LinearProgress).toHaveBeenCalledWith( + expect.objectContaining({ + variant: 'determinate', + value: 50, + }), + {}, + ) + expect(LinearProgress).toHaveBeenCalledWith( + expect.objectContaining({ + variant: 'determinate', + value: 75, + }), + {}, + ) + }) + + it('renders progress bars without labels', () => { + const props = { + syncStepTitle: 'sync step', + progressBars: [{ progress: 50 }, { progress: 75 }], + } + + render() + + // Check progress bars + expect(LinearProgress).toHaveBeenCalledTimes(2) + expect(LinearProgress).toHaveBeenCalledWith( + expect.objectContaining({ + variant: 'determinate', + value: 50, + }), + {}, + ) + expect(LinearProgress).toHaveBeenCalledWith( + expect.objectContaining({ + variant: 'determinate', + value: 75, + }), + {}, + ) + }) + + it('handles empty progressBars', () => { + const props = { syncStepTitle: 'sync step', progressBars: [] } + render() + + // Check that no progress bars are rendered + expect(LinearProgress).not.toHaveBeenCalled() + }) +}) diff --git a/src/ui/pages/visualize/Visualize.test.tsx b/src/ui/pages/visualize/Visualize.test.tsx new file mode 100644 index 00000000..d7cb6daf --- /dev/null +++ b/src/ui/pages/visualize/Visualize.test.tsx @@ -0,0 +1,96 @@ +import { render } from '@testing-library/react' +import { useNavigate } from 'react-router-dom' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import type { SurveyUnit } from '@/core/model' +import { Orchestrator } from '@/ui/components/orchestrator/Orchestrator' +import { downloadAsJson } from '@/ui/components/orchestrator/tools/functions' +import { useLoaderData } from '@/ui/routing/utils' + +import { Visualize } from './Visualize' +import { VisualizeForm } from './VisualizeForm' + +vi.mock('@/ui/routing/utils', () => ({ + useLoaderData: vi.fn(), +})) + +vi.mock('react-router-dom', () => ({ + useNavigate: vi.fn(), +})) +vi.mock('@/ui/components/orchestrator/tools/functions', () => ({ + downloadAsJson: vi.fn(), +})) +vi.mock('@/ui/components/orchestrator/Orchestrator', () => ({ + Orchestrator: vi.fn(), +})) +vi.mock('./VisualizeForm', () => ({ + VisualizeForm: vi.fn(), +})) + +describe('Visualize Component', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('renders Orchestrator when loaderData is available', async () => { + const mockLoaderData = { + source: 'mockSource', + surveyUnit: { id: 'mockSurveyUnit' }, + readonly: true, + getReferentiel: vi.fn(), + } + + vi.mocked(useLoaderData).mockReturnValue(mockLoaderData) + + render() + + expect(Orchestrator).toHaveBeenCalledWith( + expect.objectContaining({ + source: mockLoaderData.source, + surveyUnit: mockLoaderData.surveyUnit, + readonly: mockLoaderData.readonly, + onQuit: expect.any(Function), + onDefinitiveQuit: expect.any(Function), + onChangePage: undefined, + getReferentiel: mockLoaderData.getReferentiel, + }), + {}, + ) + }) + + it('renders VisualizeForm when loaderData is null', async () => { + vi.mocked(useLoaderData).mockReturnValue(null as any) // type issue even if loaderData can be null + + render() + + expect(VisualizeForm).toHaveBeenCalled() + }) + + it('calls onQuit with surveyUnit and navigates to /visualize', async () => { + const mockLoaderData = { + source: 'mockSource', + surveyUnit: { id: 'mockSurveyUnit' }, + readonly: true, + getReferentiel: vi.fn(), + } + + vi.mocked(useLoaderData).mockReturnValue(mockLoaderData) + + const mockNavigate = vi.fn() + vi.mocked(useNavigate).mockReturnValue(mockNavigate) + + render() + + // Simulate Orchestrator's onQuit call + const { onQuit } = vi.mocked(Orchestrator).mock.calls[0][0] + + if (onQuit) { + onQuit(mockLoaderData.surveyUnit as SurveyUnit) + } + + expect(downloadAsJson).toHaveBeenCalledWith({ + data: mockLoaderData.surveyUnit, + }) + expect(mockNavigate).toHaveBeenCalledWith('/visualize') + }) +}) diff --git a/src/ui/pages/visualize/Visualize.tsx b/src/ui/pages/visualize/Visualize.tsx index 581a4228..cc926bd3 100644 --- a/src/ui/pages/visualize/Visualize.tsx +++ b/src/ui/pages/visualize/Visualize.tsx @@ -1,9 +1,10 @@ -import { useLoaderData, useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import type { SurveyUnit } from '@/core/model' import { Orchestrator } from '@/ui/components/orchestrator/Orchestrator' import { downloadAsJson } from '@/ui/components/orchestrator/tools/functions' import { visualizeLoader } from '@/ui/routing/loader/visualizeLoader' +import { useLoaderData } from '@/ui/routing/utils' import { VisualizeForm } from './VisualizeForm' diff --git a/src/ui/pages/visualize/VisualizeForm.test.tsx b/src/ui/pages/visualize/VisualizeForm.test.tsx new file mode 100644 index 00000000..d6bd323a --- /dev/null +++ b/src/ui/pages/visualize/VisualizeForm.test.tsx @@ -0,0 +1,100 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { useSearchParams } from 'react-router-dom' +import { describe, expect, it, vi } from 'vitest' + +import { VisualizeForm } from './VisualizeForm' +import { getSearchParams } from './getSearchParams' + +vi.mock('@/i18n', () => ({ + useTranslation: () => ({ t: (keyMessage: string) => keyMessage }), +})) + +vi.mock('react-router-dom', () => ({ + useSearchParams: vi.fn(() => [ + new URLSearchParams(), // the first value is the URLSearchParams instance + vi.fn(), // the second value is the setter function (setSearchParams) + ]), +})) + +vi.mock('./getSearchParams', () => ({ + getSearchParams: vi.fn(), +})) + +describe('VisualizeForm Component', () => { + it('renders form fields correctly', () => { + render() + + expect(screen.getByLabelText('inputSurveyLabel')).toBeInTheDocument() + expect(screen.getByLabelText('inputDataLabel')).toBeInTheDocument() + expect(screen.getByLabelText('inputNomenclatureLabel')).toBeInTheDocument() + expect(screen.getByLabelText('readonlyLabel')).toBeInTheDocument() + expect(screen.getByText('visualizeButtonLabel')).toBeInTheDocument() + }) + + it('submits form with correct parameters', async () => { + const mockSetSearchParams = vi.fn() + vi.mocked(useSearchParams).mockReturnValue([ + new URLSearchParams(), + mockSetSearchParams, + ]) + + // mock the getSearchParams(data) value + const mockParams = { + questionnaire: 'mockSurvey', + data: 'mockData', + nomenclature: '{}', + } + vi.mocked(getSearchParams).mockReturnValue(mockParams) + + const { getByLabelText, getByRole } = render() + + // Fill out the form + fireEvent.change(getByLabelText('inputSurveyLabel'), { + target: { value: 'my-survey' }, + }) + fireEvent.change(getByLabelText('inputDataLabel'), { + target: { value: 'my-data' }, + }) + fireEvent.change(getByLabelText('inputNomenclatureLabel'), { + target: { value: '{}' }, + }) + const submitButton = getByRole('button', { name: 'visualizeButtonLabel' }) + fireEvent.click(submitButton) + + await waitFor(() => { + expect(getSearchParams).toHaveBeenCalledWith({ + questionnaire: 'my-survey', + data: 'my-data', + nomenclature: {}, + readonly: false, + }) + expect(mockSetSearchParams).toHaveBeenCalledWith(mockParams) + }) + }) + + it('handles readonly switch', async () => { + const mockSetSearchParams = vi.fn() + vi.mocked(useSearchParams).mockReturnValue([ + new URLSearchParams(), + mockSetSearchParams, + ]) + + const { getByRole } = render() + + fireEvent.click(getByRole('checkbox'), { name: 'readonlyLabel' }) + + fireEvent.click( + screen.getByRole('button', { name: 'visualizeButtonLabel' }), + ) + + // Ensure the readonly value is sent as true when the switch is toggled + await waitFor(() => { + expect(getSearchParams).toHaveBeenCalledWith({ + questionnaire: '', + data: '', + nomenclature: null, + readonly: true, + }) + }) + }) +}) diff --git a/src/ui/pages/visualize/getSearchParams.test.ts b/src/ui/pages/visualize/getSearchParams.test.ts new file mode 100644 index 00000000..830d8c62 --- /dev/null +++ b/src/ui/pages/visualize/getSearchParams.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from 'vitest' + +import type { FormValues } from './VisualizeForm' +import { getSearchParams } from './getSearchParams' + +describe('getSearchParams', () => { + it('should return all fields when all fields are provided', () => { + const input: FormValues = { + questionnaire: 'my-survey', + data: 'my-data', + nomenclature: { key: 'value' }, + readonly: false, + } + + const result = getSearchParams(input) + + expect(result).toEqual({ + questionnaire: 'my-survey', + data: 'my-data', + nomenclature: '{"key":"value"}', + readonly: 'false', + }) + }) + + it('should exclude optional fields if they are undefined', () => { + const input: FormValues = { + questionnaire: 'my-survey', + data: undefined, + nomenclature: undefined, + readonly: false, + } + + const result = getSearchParams(input) + + expect(result).toEqual({ + questionnaire: 'my-survey', + readonly: 'false', + }) + }) + + it('should exclude optional fields if they are empty strings', () => { + const input: FormValues = { + questionnaire: '', + data: '', + nomenclature: undefined, + readonly: false, + } + + const result = getSearchParams(input) + + expect(result).toEqual({ + readonly: 'false', + }) + }) + + it('should handle nomenclature objects correctly', () => { + const input: FormValues = { + questionnaire: 'my-survey', + data: 'my-data', + nomenclature: { key1: 'value1', key2: 'value2' }, + readonly: false, + } + + const result = getSearchParams(input) + + expect(result).toEqual({ + questionnaire: 'my-survey', + data: 'my-data', + nomenclature: '{"key1":"value1","key2":"value2"}', + readonly: 'false', + }) + }) + + it('should handle readonly values correctly', () => { + const input: FormValues = { + questionnaire: '', + readonly: false, + } + + const input2: FormValues = { + questionnaire: '', + readonly: true, + } + + const result = getSearchParams(input) + const result2 = getSearchParams(input2) + + expect(result).toEqual({ + readonly: 'false', + }) + expect(result2).toEqual({ + readonly: 'true', + }) + }) +})