From 3074620dba3d1fdf2e4f59f6440665db8e142012 Mon Sep 17 00:00:00 2001 From: Ian Bolton Date: Mon, 11 Mar 2024 11:53:03 -0400 Subject: [PATCH] :ghost: Add unit tests and hook for useAssessmentStatus Signed-off-by: Ian Bolton --- client/src/app/hooks/useAssessmentStatus.ts | 76 +++++ .../application-assessment-status.tsx | 98 +----- .../application-assessment-status.test.tsx | 317 ++++++++++++++++++ client/src/app/test-config/test-utils.tsx | 31 ++ 4 files changed, 438 insertions(+), 84 deletions(-) create mode 100644 client/src/app/hooks/useAssessmentStatus.ts create mode 100644 client/src/app/pages/applications/components/application-assessment-status/tests/application-assessment-status.test.tsx diff --git a/client/src/app/hooks/useAssessmentStatus.ts b/client/src/app/hooks/useAssessmentStatus.ts new file mode 100644 index 0000000000..866ef24525 --- /dev/null +++ b/client/src/app/hooks/useAssessmentStatus.ts @@ -0,0 +1,76 @@ +// 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 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 assessmentsFromArchetypesCount = assessmentsWithArchetypes.filter( + ({ assessments }) => assessments.some((assessment) => assessment.required) + ).length; + + const assessedArchetypesCount = assessmentsWithArchetypes.filter( + ({ assessments, inheritedArchetype }) => + assessments.some( + (assessment) => + assessment.required && + assessment.status === "complete" && + inheritedArchetype.assessed + ) + ).length; + + const hasApplicationAssessmentInProgress = applicationAssessments?.some( + (assessment: Assessment) => + assessment.status === "started" || + assessment.status === "empty" || + assessment.status === "complete" + ); + const isDirectlyAssessed = + (application.assessed && (application.assessments?.length ?? 0) > 0) ?? + false; + + return { + allArchetypesAssessed, + countOfFullyAssessedArchetypes: assessedArchetypesCount, + countOfArchetypesWithRequiredAssessments: assessmentsFromArchetypesCount, + hasApplicationAssessmentInProgress, + isApplicationDirectlyAssessed: 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..79580d3890 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,28 +29,23 @@ 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, + countOfFullyAssessedArchetypes, + countOfArchetypesWithRequiredAssessments, + hasApplicationAssessmentInProgress, + isApplicationDirectlyAssessed, } = assessmentStatusInfo; - if (isDirectlyAssessed) { + if (isApplicationDirectlyAssessed) { statusPreset = "Completed"; } else if (allArchetypesAssessed) { statusPreset = "InheritedAssessments"; - tooltipCount = assessedArchetypesWithARequiredAssessment.length; - } else if (hasInProgressOrNotStartedRequiredAssessments) { + tooltipCount = countOfFullyAssessedArchetypes; + } else if (countOfArchetypesWithRequiredAssessments > 0) { statusPreset = "InProgressInheritedAssessments"; - tooltipCount = assessedArchetypesWithARequiredAssessment.length; - } else if ( - applicationAssessments?.some( - (assessment) => assessment.status === "started" - ) - ) { + tooltipCount = countOfArchetypesWithRequiredAssessments; + } else if (hasApplicationAssessmentInProgress) { 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..a656c84fef --- /dev/null +++ b/client/src/app/pages/applications/components/application-assessment-status/tests/application-assessment-status.test.tsx @@ -0,0 +1,317 @@ +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({ + allArchetypesAssessed: false, + countOfFullyAssessedArchetypes: 0, + countOfArchetypesWithRequiredAssessments: 0, + hasApplicationAssessmentInProgress: true, + isApplicationDirectlyAssessed: false, + }); + }); + + 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({ + allArchetypesAssessed: false, + countOfFullyAssessedArchetypes: 0, + countOfArchetypesWithRequiredAssessments: 0, + hasApplicationAssessmentInProgress: true, + isApplicationDirectlyAssessed: true, + }); + }); + it("Correctly calculates status given two inherited archetype; One with a complete state and one with started state.", () => { + const arch1Assessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + }), + ]; + const arch2Assessments = [ + createMockAssessment({ + id: 2, + archetype: { id: 2, name: "archetype2" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "started", + }), + ]; + const mockAssessments = [...arch1Assessments, ...arch2Assessments]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: arch1Assessments, + assessed: true, + }), + createMockArchetype({ + id: 2, + name: "archetype2", + applications: [{ id: 1, name: "app1" }], + assessments: arch2Assessments, + assessed: false, + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [ + { id: 1, name: "archetype1" }, + { id: 2, name: "archetype2" }, + ], + assessed: false, + }); + + const { result } = renderHook(() => + useAssessmentStatus(mockAssessments, mockArchetypes, mockApplication) + ); + expect(result.current).toEqual({ + allArchetypesAssessed: false, + countOfFullyAssessedArchetypes: 1, + countOfArchetypesWithRequiredAssessments: 2, + hasApplicationAssessmentInProgress: false, + isApplicationDirectlyAssessed: false, + }); + }); + + it("Correctly calculates status given a single inherited archetype with a complete state.", () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: mockAssessments, + assessed: true, + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [{ id: 1, name: "archetype1" }], + assessed: false, + }); + + const { result } = renderHook(() => + useAssessmentStatus(mockAssessments, mockArchetypes, mockApplication) + ); + expect(result.current).toEqual({ + allArchetypesAssessed: true, + countOfFullyAssessedArchetypes: 1, + countOfArchetypesWithRequiredAssessments: 1, + hasApplicationAssessmentInProgress: false, + isApplicationDirectlyAssessed: false, + }); + }); + it("Correctly calculates status given 1 started assessment for an applications only archetype.", () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "started", + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: [...mockAssessments], + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [{ id: 1, name: "archetype1" }], + assessed: false, + }); + + const { result } = renderHook(() => + useAssessmentStatus(mockAssessments, mockArchetypes, mockApplication) + ); + expect(result.current).toEqual({ + allArchetypesAssessed: false, + countOfFullyAssessedArchetypes: 0, + countOfArchetypesWithRequiredAssessments: 1, + hasApplicationAssessmentInProgress: false, + isApplicationDirectlyAssessed: false, + }); + }); + it("Correctly calculates status given one complete assessment for an application's inherited archetype with no direct assessment", () => { + const mockAssessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: mockAssessments, + assessed: true, + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [{ id: 1, name: "archetype1" }], + assessed: false, + }); + + const { result } = renderHook(() => + useAssessmentStatus(mockAssessments, mockArchetypes, mockApplication) + ); + expect(result.current).toEqual({ + allArchetypesAssessed: true, + countOfFullyAssessedArchetypes: 1, + countOfArchetypesWithRequiredAssessments: 1, + hasApplicationAssessmentInProgress: false, + isApplicationDirectlyAssessed: false, + }); + }); + it("Correctly calculates status given one complete assessment for an application's inherited archetype with a direct assessment", () => { + const archetypeAssessments = [ + createMockAssessment({ + id: 1, + archetype: { id: 1, name: "archetype1" }, + questionnaire: { id: 1, name: "questionnaire1" }, + status: "complete", + }), + ]; + + const mockArchetypes = [ + createMockArchetype({ + id: 1, + name: "archetype1", + applications: [{ id: 1, name: "app1" }], + assessments: archetypeAssessments, + assessed: true, + }), + ]; + const applicationAssessments = [ + createMockAssessment({ + id: 2, + application: { id: 1, name: "app1" }, + questionnaire: { id: 2, name: "questionnaire2" }, + status: "complete", + }), + ]; + + const mockApplication = createMockApplication({ + id: 1, + name: "app1", + archetypes: [{ id: 1, name: "archetype1" }], + assessed: true, + assessments: applicationAssessments, + }); + + const mockAssessments = [ + ...archetypeAssessments, + ...applicationAssessments, + ]; + + const { result } = renderHook(() => + useAssessmentStatus(mockAssessments, mockArchetypes, mockApplication) + ); + expect(result.current).toEqual({ + allArchetypesAssessed: true, + countOfFullyAssessedArchetypes: 1, + countOfArchetypesWithRequiredAssessments: 1, + hasApplicationAssessmentInProgress: true, + isApplicationDirectlyAssessed: true, + }); + }); +}); 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; +};