diff --git a/client/src/app/hooks/useAssessmentStatus.ts b/client/src/app/hooks/useAssessmentStatus.ts new file mode 100644 index 0000000000..211237fd37 --- /dev/null +++ b/client/src/app/hooks/useAssessmentStatus.ts @@ -0,0 +1,94 @@ +// hooks/useAssessmentStatus.js +import { Assessment, Archetype, Application } from "@app/api/models"; +import { useMemo } from "react"; + +export const useAssessmentStatus = ( + assessments: Assessment[], + archetypes: Archetype[], + application: Application +) => { + return useMemo(() => { + const applicationAssessments = assessments?.filter( + (assessment: Assessment) => assessment.application?.id === application.id + ); + const inheritedArchetypes = archetypes?.filter( + (archetype: Archetype) => + archetype.applications?.map((app) => app.id).includes(application.id) + ); + + const assessmentsWithArchetypes = inheritedArchetypes.map( + (inheritedArchetype) => ({ + inheritedArchetype, + assessments: assessments.filter( + (assessment) => assessment.archetype?.id === inheritedArchetype.id + ), + }) + ); + + const someArchetypesAssessed = assessmentsWithArchetypes.some( + ({ assessments }) => assessments.length > 0 + ); + + const allArchetypesAssessed = + assessmentsWithArchetypes.length > 0 && + assessmentsWithArchetypes.every(({ inheritedArchetype, assessments }) => { + const requiredAssessments = assessments.filter( + (assessment) => assessment.required + ); + return ( + inheritedArchetype.assessed && + assessments.length > 0 && + requiredAssessments.length > 0 && + requiredAssessments.every( + (assessment) => assessment.status === "complete" + ) + ); + }); + + const hasInProgressOrNotStartedRequiredAssessments = + assessmentsWithArchetypes.some(({ assessments }) => + assessments.some( + (assessment) => + assessment.required && + ["empty", "started"].includes(assessment.status) + ) + ); + + const assessedArchetypesWithARequiredAssessment = + assessmentsWithArchetypes.filter(({ assessments, inheritedArchetype }) => + assessments.some( + (assessment) => + assessment.required && + assessment.status === "complete" && + inheritedArchetype.assessed + ) + ); + const assessedArchetypeCount = + inheritedArchetypes?.filter( + (inheritedArchetype) => + inheritedArchetype?.assessments?.length ?? + (0 > 0 && inheritedArchetype.assessed) + ).length || 0; + + const hasAssessmentInProgress = applicationAssessments?.some( + (assessment: Assessment) => + assessment.status === "started" || + assessment.status === "empty" || + assessment.status === "complete" + ); + const isDirectlyAssessed = + application.assessed && (application.assessments?.length ?? 0) > 0; + + return { + applicationAssessments, + assessmentsWithArchetypes, + someArchetypesAssessed, + allArchetypesAssessed, + hasInProgressOrNotStartedRequiredAssessments, + assessedArchetypesWithARequiredAssessment, + assessedArchetypeCount, + hasAssessmentInProgress, + isDirectlyAssessed, + }; + }, [assessments, archetypes, application]); +}; diff --git a/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx b/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx index a6a0df6ab7..c012278b3f 100644 --- a/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx +++ b/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { Application, Archetype, Assessment } from "@app/api/models"; import { IconedStatus, IconedStatusPreset } from "@app/components/IconedStatus"; import { Spinner } from "@patternfly/react-core"; +import { useAssessmentStatus } from "@app/hooks/useAssessmentStatus"; interface ApplicationAssessmentStatusProps { application: Application; assessments: Assessment[]; @@ -15,77 +16,11 @@ export const ApplicationAssessmentStatus: React.FC< > = ({ application, assessments, archetypes, isLoading }) => { const { t } = useTranslation(); - const applicationAssessments = assessments?.filter( - (assessment: Assessment) => assessment.application?.id === application.id + const assessmentStatusInfo = useAssessmentStatus( + assessments, + archetypes, + application ); - const inheritedArchetypes = archetypes?.filter( - (archetype: Archetype) => - archetype.applications?.map((app) => app.id).includes(application.id) - ); - const assessmentStatusInfo = React.useMemo(() => { - const assessmentsWithArchetypes = inheritedArchetypes.map( - (inheritedArchetype) => ({ - inheritedArchetype, - assessments: assessments.filter( - (assessment) => assessment.archetype?.id === inheritedArchetype.id - ), - }) - ); - - const someArchetypesAssessed = assessmentsWithArchetypes.some( - ({ assessments }) => assessments.length > 0 - ); - - const allArchetypesAssessed = - assessmentsWithArchetypes.length > 0 && - assessmentsWithArchetypes.every(({ inheritedArchetype, assessments }) => { - const requiredAssessments = assessments.filter( - (assessment) => assessment.required - ); - return ( - inheritedArchetype.assessed && - assessments.length > 0 && - requiredAssessments.length > 0 && - requiredAssessments.every( - (assessment) => assessment.status === "complete" - ) - ); - }); - - const hasInProgressOrNotStartedRequiredAssessments = - assessmentsWithArchetypes.some(({ assessments }) => - assessments.some( - (assessment) => - assessment.required && - ["empty", "started"].includes(assessment.status) - ) - ); - - const assessedArchetypesWithARequiredAssessment = - assessmentsWithArchetypes.filter(({ assessments, inheritedArchetype }) => - assessments.some( - (assessment) => - assessment.required && - assessment.status === "complete" && - inheritedArchetype.assessed - ) - ); - const assessedArchetypeCount = - inheritedArchetypes?.filter( - (inheritedArchetype) => - inheritedArchetype?.assessments?.length ?? - (0 > 0 && inheritedArchetype.assessed) - ).length || 0; - - return { - assessmentsWithArchetypes, - someArchetypesAssessed, - allArchetypesAssessed, - hasInProgressOrNotStartedRequiredAssessments, - assessedArchetypesWithARequiredAssessment, - assessedArchetypeCount, - }; - }, [inheritedArchetypes, assessments]); if (isLoading) { return ; @@ -94,13 +29,12 @@ export const ApplicationAssessmentStatus: React.FC< let statusPreset: IconedStatusPreset = "NotStarted"; // Default status let tooltipCount: number = 0; - const isDirectlyAssessed = - application.assessed && (application.assessments?.length ?? 0) > 0; - const { allArchetypesAssessed, assessedArchetypesWithARequiredAssessment, hasInProgressOrNotStartedRequiredAssessments, + hasAssessmentInProgress, + isDirectlyAssessed, } = assessmentStatusInfo; if (isDirectlyAssessed) { @@ -111,11 +45,7 @@ export const ApplicationAssessmentStatus: React.FC< } else if (hasInProgressOrNotStartedRequiredAssessments) { statusPreset = "InProgressInheritedAssessments"; tooltipCount = assessedArchetypesWithARequiredAssessment.length; - } else if ( - applicationAssessments?.some( - (assessment) => assessment.status === "started" - ) - ) { + } else if (hasAssessmentInProgress) { statusPreset = "InProgress"; } return ; diff --git a/client/src/app/pages/applications/components/application-assessment-status/tests/application-assessment-status.test.tsx b/client/src/app/pages/applications/components/application-assessment-status/tests/application-assessment-status.test.tsx new file mode 100644 index 0000000000..c5cc42248d --- /dev/null +++ b/client/src/app/pages/applications/components/application-assessment-status/tests/application-assessment-status.test.tsx @@ -0,0 +1,136 @@ +import { renderHook } from "@testing-library/react-hooks"; +import "@testing-library/jest-dom"; +import { useAssessmentStatus } from "@app/hooks/useAssessmentStatus"; +import { + createMockApplication, + createMockArchetype, + createMockAssessment, +} from "@app/test-config/test-utils"; + +describe("useAssessmentStatus", () => { + it("Correctly calculates status given one started assessment and one complete assessment for an application", () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + application: { id: 1, name: "app1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "started", + }), + + createMockAssessment({ + id: 2, + application: { id: 1, name: "app1" }, + questionnaire: { id: 2, name: "questionnaire2" }, + status: "complete", + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + }), + ]; + + const mockApplication = createMockApplication({ id: 1, name: "app1" }); + + const { result } = renderHook(() => + useAssessmentStatus(mockAssessments, mockArchetypes, mockApplication) + ); + expect(result.current).toEqual({ + applicationAssessments: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + required: true, + status: "started", + }), + expect.objectContaining({ + id: expect.any(Number), + required: true, + status: "complete", + }), + ]), + assessmentsWithArchetypes: expect.arrayContaining([ + expect.objectContaining({ + inheritedArchetype: expect.any(Object), + assessments: expect.any(Array), + }), + ]), + someArchetypesAssessed: false, + allArchetypesAssessed: false, + hasInProgressOrNotStartedRequiredAssessments: false, + assessedArchetypesWithARequiredAssessment: expect.any(Array), + assessedArchetypeCount: 0, + hasAssessmentInProgress: true, + }); + }); + + it("Correctly calculates status given two complete assessments for an application", () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + application: { id: 1, name: "app1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + }), + + createMockAssessment({ + id: 2, + application: { id: 1, name: "app1" }, + questionnaire: { id: 2, name: "questionnaire2" }, + status: "complete", + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + assessed: true, + assessments: mockAssessments, + }); + + const { result } = renderHook(() => + useAssessmentStatus(mockAssessments, mockArchetypes, mockApplication) + ); + + expect(result.current).toEqual({ + applicationAssessments: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + required: true, + status: "complete", + }), + expect.objectContaining({ + id: expect.any(Number), + required: true, + status: "complete", + }), + ]), + assessmentsWithArchetypes: expect.arrayContaining([ + expect.objectContaining({ + inheritedArchetype: expect.any(Object), + assessments: expect.any(Array), + }), + ]), + someArchetypesAssessed: false, + allArchetypesAssessed: false, + hasInProgressOrNotStartedRequiredAssessments: false, + assessedArchetypesWithARequiredAssessment: expect.any(Array), + assessedArchetypeCount: 0, + hasAssessmentInProgress: true, + isDirectlyAssessed: true, + }); + }); + it("Correctly calculates status given no complete assessments for an application", () => {}); + it("Correctly calculates status given one complete assessment for an application's inherited archetype with no direct assessment", () => {}); + it("Correctly calculates status given one complete assessment for an application's inherited archetype with a direct assessment", () => {}); +}); diff --git a/client/src/app/test-config/test-utils.tsx b/client/src/app/test-config/test-utils.tsx index a65555d633..38cf8d5224 100644 --- a/client/src/app/test-config/test-utils.tsx +++ b/client/src/app/test-config/test-utils.tsx @@ -1,6 +1,7 @@ import React, { FC, ReactElement } from "react"; import { render, RenderOptions } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Application, Archetype, Assessment } from "@app/api/models"; const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => { const queryClient = new QueryClient(); @@ -19,3 +20,33 @@ export * from "@testing-library/react"; // override render method export { customRender as render }; + +export const createMockAssessment = ( + overrides: Partial = {} +): Assessment => { + return { + id: Math.random(), + name: "Default name", + description: "Default description", + required: true, + ...overrides, + } as Assessment; +}; + +export const createMockApplication = (overrides: Partial = {}) => { + return { + id: Math.random(), + name: "Default name", + description: "Default description", + ...overrides, + } as Application; +}; + +export const createMockArchetype = (overrides: Partial = {}) => { + return { + id: Math.random(), + name: "Default name", + description: "Default description", + ...overrides, + } as Archetype; +};