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;
+};