Skip to content

Commit

Permalink
👻 Add unit tests and hook for useAssessmentStatus
Browse files Browse the repository at this point in the history
Signed-off-by: Ian Bolton <[email protected]>
  • Loading branch information
ibolton336 committed Mar 11, 2024
1 parent 3e3d690 commit 426f36b
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 78 deletions.
94 changes: 94 additions & 0 deletions client/src/app/hooks/useAssessmentStatus.ts
Original file line number Diff line number Diff line change
@@ -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]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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 <Spinner size="sm" />;
Expand All @@ -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) {
Expand All @@ -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 <IconedStatus preset={statusPreset} tooltipCount={tooltipCount} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -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", () => {});
});
31 changes: 31 additions & 0 deletions client/src/app/test-config/test-utils.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -19,3 +20,33 @@ export * from "@testing-library/react";

// override render method
export { customRender as render };

export const createMockAssessment = (
overrides: Partial<Assessment> = {}
): Assessment => {
return {
id: Math.random(),
name: "Default name",
description: "Default description",
required: true,
...overrides,
} as Assessment;
};

export const createMockApplication = (overrides: Partial<Application> = {}) => {
return {
id: Math.random(),
name: "Default name",
description: "Default description",
...overrides,
} as Application;
};

export const createMockArchetype = (overrides: Partial<Archetype> = {}) => {
return {
id: Math.random(),
name: "Default name",
description: "Default description",
...overrides,
} as Archetype;
};

0 comments on commit 426f36b

Please sign in to comment.