{
it("handles undefined case", () => {
@@ -58,3 +69,45 @@ describe("getMedicationDisplayName", () => {
).toBe("Unknown medication name - ABC code 123");
});
});
+
+describe("returnTableFromJson", () => {
+ it("returns an HTML representation of the table", () => {
+ const tableJson = {
+ resultName: "test-name",
+ tables: [
+ [
+ {
+ col1: { value: "val1", metadata: {} },
+ col2: { value: "val2", metadata: {} },
+ },
+ ],
+ ],
+ };
+
+ const result = returnTableFromJson(tableJson as TableJson);
+ render(result);
+ expect(screen.getByText("test-name")).toBeInTheDocument();
+ expect(screen.getByText("col1")).toBeInTheDocument();
+ expect(screen.getByText("col2")).toBeInTheDocument();
+ expect(screen.getByText("val1")).toBeInTheDocument();
+ expect(screen.getByText("val2")).toBeInTheDocument();
+ });
+});
+
+describe("returnHtmlTableContent", () => {
+ it("returns the html tables with a title", () => {
+ const result = returnHtmlTableContent(
+ BundleLabNoLabIds as Bundle,
+ mappings["labResultDiv"],
+ "test-title",
+ );
+
+ render(result);
+ expect(screen.getByText("test-title")).toBeInTheDocument();
+ expect(screen.getByText("SARS-CoV-2, NAA CL")).toBeInTheDocument();
+ expect(
+ screen.getByText("Symptomatic as defined by CDC?"),
+ ).toBeInTheDocument();
+ expect(screen.getAllByText("2022-04-21T21:02:00.000Z")).toHaveLength(2);
+ });
+});
diff --git a/containers/ecr-viewer/src/app/tests/services/ecrSummaryService.test.tsx b/containers/ecr-viewer/src/app/tests/services/ecrSummaryService.test.tsx
index 125fc200d..316f49de6 100644
--- a/containers/ecr-viewer/src/app/tests/services/ecrSummaryService.test.tsx
+++ b/containers/ecr-viewer/src/app/tests/services/ecrSummaryService.test.tsx
@@ -6,6 +6,7 @@ import {
import BundleWithClinicalInfo from "@/app/tests/assets/BundleClinicalInfo.json";
import { evaluateEcrSummaryRelevantLabResults } from "@/app/services/ecrSummaryService";
import BundleLab from "@/app/tests/assets/BundleLab.json";
+import BundleLabNoLabIds from "@/app/tests/assets/BundleLabNoLabIds.json";
import BundleEcrSummary from "@/app/tests/assets/BundleEcrSummary.json";
import { Bundle } from "fhir/r4";
import { render, screen } from "@testing-library/react";
@@ -102,6 +103,23 @@ describe("Evaluate eCR Summary Relevant Lab Results", () => {
expect(screen.getByText("Cytogenomic SNP microarray")).toBeInTheDocument();
});
+ it("should return all lab results when lab results are not LabReportElementData", () => {
+ const result = evaluateEcrSummaryRelevantLabResults(
+ BundleLabNoLabIds as unknown as Bundle,
+ mappings,
+ "840539006",
+ );
+ expect(result).toHaveLength(2); // 1 result, plus last item is divider line
+
+ render(result[0].value);
+ expect(screen.getByRole("button")).toBeInTheDocument();
+ expect(screen.getByText("SARS-CoV-2, NAA CL")).toBeInTheDocument();
+ expect(
+ screen.getByText("Symptomatic as defined by CDC?"),
+ ).toBeInTheDocument();
+ expect(screen.getAllByText("2022-04-21T21:02:00.000Z")).toHaveLength(2);
+ });
+
it("should not include the last empty divider line when lastDividerLine is false", () => {
const result = evaluateEcrSummaryRelevantLabResults(
BundleLab as unknown as Bundle,
diff --git a/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx b/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx
index aeebf1d2f..c6a1415a3 100644
--- a/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx
+++ b/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx
@@ -486,6 +486,71 @@ describe("formatTablesToJSON", () => {
expect(result).toEqual(expectedResult);
});
+ it("
{name}", () => {
+ const tableString =
+ '
Empty HeaderFuture TestsPending Tests< /br>
Text HeaderNo table here
Patient Instructions';
+ const expectedResult = [
+ {
+ resultName: "Future Tests",
+ tables: [[{ Name: { metadata: {}, value: "test1" } }]],
+ },
+ {
+ resultName: "Pending Tests",
+ tables: [[{ Name: { metadata: {}, value: "test2" } }]],
+ },
+ {
+ resultName: "Patient Instructions",
+ tables: [
+ [
+ {
+ "Unknown Header": {
+ metadata: { id: "potpatinstr-1" },
+ value: "instruction",
+ },
+ },
+ ],
+ ],
+ },
+ ];
+ const result = formatTablesToJSON(tableString);
+
+ expect(result).toEqual(expectedResult);
+ });
+
+ it("
", () => {
+ const tableString =
+ "
";
+ const expectedResult = [
+ {
+ resultId: undefined,
+ resultName: "",
+ tables: [[{ Name: { metadata: {}, value: "test1" } }]],
+ },
+ {
+ resultId: undefined,
+ resultName: "",
+ tables: [[{ Name: { metadata: {}, value: "test2" } }]],
+ },
+ {
+ resultId: undefined,
+ resultName: "",
+ tables: [
+ [
+ {
+ "Unknown Header": {
+ metadata: { id: "potpatinstr-1" },
+ value: "instruction",
+ },
+ },
+ ],
+ ],
+ },
+ ];
+ const result = formatTablesToJSON(tableString);
+
+ expect(result).toEqual(expectedResult);
+ });
+
it("should return an empty array when HTML string input has no tables", () => {
const htmlString =
"
Hello, World!
This HTML string has no tables.
";
diff --git a/containers/ecr-viewer/src/app/tests/services/labsService.test.tsx b/containers/ecr-viewer/src/app/tests/services/labsService.test.tsx
index ea05560b0..2f7491f06 100644
--- a/containers/ecr-viewer/src/app/tests/services/labsService.test.tsx
+++ b/containers/ecr-viewer/src/app/tests/services/labsService.test.tsx
@@ -1,5 +1,7 @@
import { loadYamlConfig } from "@/app/api/utils";
import BundleLab from "../assets/BundleLab.json";
+import BundleLabNoLabIds from "../assets/BundleLabNoLabIds.json";
+import BundleLabInvalidResultsDiv from "../assets/BundleLabInvalidResultsDiv.json";
import { Bundle, Observation, Organization } from "fhir/r4";
import { evaluate } from "fhirpath";
import { render, screen } from "@testing-library/react";
@@ -18,8 +20,10 @@ import {
combineOrgAndReportData,
evaluateLabInfoData,
findIdenticalOrg,
+ isLabReportElementDataList,
} from "@/app/services/labsService";
import { AccordionLabResults } from "@/app/view-data/components/AccordionLabResults";
+import { DisplayDataProps } from "@/app/view-data/components/DataDisplay";
const mappings = loadYamlConfig();
@@ -206,7 +210,7 @@ describe("Labs Utils", () => {
});
describe("getLabJsonObject", () => {
- it("returns correct Json Object", () => {
+ it("returns correct Json Object for table with data-id", () => {
const expectedResult = labReportNormalJsonObject;
const result = getLabJsonObject(
@@ -217,6 +221,69 @@ describe("Labs Utils", () => {
expect(result).toStrictEqual(expectedResult);
});
+
+ it("returns correct Json Object for table without data-id", () => {
+ const labReportWithoutIds = (
+ evaluate(
+ BundleLabNoLabIds,
+ "Bundle.entry.resource.where(resourceType = 'DiagnosticReport').where(id = '97d3b36a-f833-2f3c-b456-abeb1fd342e4')",
+ ) as LabReport[]
+ )[0];
+ const labReportJsonObjectWithoutId = {
+ resultId: undefined,
+ resultName: "",
+ tables: [
+ [
+ {
+ "Lab Test Name": {
+ metadata: {},
+ value: "SARS-CoV-2, NAA CL",
+ },
+ "Lab Test Result Date": {
+ metadata: {},
+ value: "2022-04-21T21:02:00.000Z",
+ },
+ "Lab Test Result Value": {
+ metadata: {},
+ value: "POS",
+ },
+ },
+ {
+ "Lab Test Name": {
+ metadata: {},
+ value: "Symptomatic as defined by CDC?",
+ },
+ "Lab Test Result Date": {
+ metadata: {},
+ value: "2022-04-21T21:02:00.000Z",
+ },
+ "Lab Test Result Value": {
+ metadata: {},
+ value: "YES",
+ },
+ },
+ ],
+ ],
+ };
+
+ const result = getLabJsonObject(
+ labReportWithoutIds,
+ BundleLabNoLabIds as Bundle,
+ mappings,
+ );
+
+ expect(result).toStrictEqual(labReportJsonObjectWithoutId);
+ });
+
+ it("returns empty object if lab results html contains no tables", () => {
+ const result = getLabJsonObject(
+ labReportNormal,
+ BundleLabInvalidResultsDiv as unknown as Bundle,
+ mappings,
+ );
+
+ expect(result).toStrictEqual({});
+ });
});
describe("checkAbnormalTag", () => {
@@ -434,7 +501,7 @@ describe("Evaluate Organization with ID", () => {
});
describe("Evaluate the lab info section", () => {
- it("should return a list of objects", () => {
+ it("should return a list of LabReportElementData if the lab results in the HTML table have ID's", () => {
const result = evaluateLabInfoData(
BundleLab as unknown as Bundle,
evaluate(BundleLab, mappings["diagnosticReports"]),
@@ -443,6 +510,17 @@ describe("Evaluate the lab info section", () => {
expect(result[0]).toHaveProperty("diagnosticReportDataElements");
expect(result[0]).toHaveProperty("organizationDisplayDataProps");
});
+
+ it("should return a list of DisplayDataProps if the lab results in the HTML table do not have ID's", () => {
+ const result = evaluateLabInfoData(
+ BundleLabNoLabIds as unknown as Bundle,
+ evaluate(BundleLabNoLabIds, mappings["diagnosticReports"]),
+ mappings,
+ );
+ expect(result[0]).toHaveProperty("title");
+ expect(result[0]).toHaveProperty("value");
+ });
+
it("should properly count the number of labs", () => {
const result = evaluateLabInfoData(
BundleLab as unknown as Bundle,
@@ -606,3 +684,25 @@ describe("Find Identical Org", () => {
).not.toBeDefined();
});
});
+
+describe("isLabReportElementDataList", () => {
+ it("returns true when the input is a list of LabReportElementData", () => {
+ const actual = isLabReportElementDataList([
+ {
+ diagnosticReportDataElements: [
+ { type: "test-type", props: "test-props", key: "test-key" },
+ ],
+ organizationId: "test-id",
+ organizationDisplayDataProps: [{} as DisplayDataProps],
+ },
+ ]);
+ expect(actual).toBe(true);
+ });
+
+ it("returns false when the input is NOT a list of LabReportElementData", () => {
+ const actual = isLabReportElementDataList([
+ { title: "test-title", value: "test-value" },
+ ]);
+ expect(actual).toBe(false);
+ });
+});
diff --git a/containers/ecr-viewer/src/app/view-data/components/AccordionLabResults.tsx b/containers/ecr-viewer/src/app/view-data/components/AccordionLabResults.tsx
index 13dd8a672..c84c61e20 100644
--- a/containers/ecr-viewer/src/app/view-data/components/AccordionLabResults.tsx
+++ b/containers/ecr-viewer/src/app/view-data/components/AccordionLabResults.tsx
@@ -1,4 +1,5 @@
import { Accordion, HeadingLevel, Tag } from "@trussworks/react-uswds";
+import classNames from "classnames";
import React from "react";
interface AccordionLabResultsProps {
@@ -8,6 +9,7 @@ interface AccordionLabResultsProps {
organizationId: string;
collapsedByDefault?: boolean;
headingLevel?: HeadingLevel;
+ className?: string;
}
/**
@@ -19,6 +21,7 @@ interface AccordionLabResultsProps {
* @param props.organizationId - The id of the organization you are getting lab results for.
* @param props.collapsedByDefault - Whether or not to collapse by default for the accordion
* @param props.headingLevel - Heading level for the Accordion menu title.
+ * @param props.className - Classnames to be applied to accordion.
* @returns React element representing the AccordionLabResults component.
*/
export const AccordionLabResults: React.FC
= ({
@@ -28,6 +31,7 @@ export const AccordionLabResults: React.FC = ({
organizationId,
collapsedByDefault = false,
headingLevel = "h5",
+ className = "",
}: AccordionLabResultsProps): React.JSX.Element => {
return (
= ({
expanded: collapsedByDefault,
id: title,
headingLevel,
- className: `acc_item_${organizationId} side-nav-ignore`,
+ className: classNames(
+ `acc_item_${organizationId} side-nav-ignore`,
+ className,
+ ),
},
]}
className={`accordion-rr accordion_${organizationId} margin-bottom-3`}
diff --git a/containers/ecr-viewer/src/app/view-data/components/LabInfo.tsx b/containers/ecr-viewer/src/app/view-data/components/LabInfo.tsx
index c0fe05a42..09e67dcc2 100644
--- a/containers/ecr-viewer/src/app/view-data/components/LabInfo.tsx
+++ b/containers/ecr-viewer/src/app/view-data/components/LabInfo.tsx
@@ -1,79 +1,96 @@
import {
- AccordionSection,
AccordionH4,
AccordionDiv,
+ AccordionSection,
} from "../component-utils";
import React from "react";
-import { ExpandCollapseButtons } from "@/app/view-data/components/ExpandCollapseButtons";
-import { formatString } from "@/app/services/formatService";
-import { LabReportElementData } from "@/app/services/labsService";
import {
DataDisplay,
+ DataTableDisplay,
DisplayDataProps,
} from "@/app/view-data/components/DataDisplay";
+import {
+ isLabReportElementDataList,
+ LabReportElementData,
+} from "@/app/services/labsService";
+import { formatString } from "@/app/services/formatService";
+import { ExpandCollapseButtons } from "./ExpandCollapseButtons";
interface LabInfoProps {
- labResults: LabReportElementData[];
+ labResults: DisplayDataProps[] | LabReportElementData[];
}
/**
- * Renders lab information and RR info in an accordion section.
- * @param props - The props object.
- * @param props.labResults - Array of Lab result items.
- * @returns React element representing the LabInfo component.
+ * Functional component for displaying clinical information.
+ * @param props - Props containing clinical information.
+ * @param props.labResults - some props
+ * @returns The JSX element representing the clinical information.
*/
-export const LabInfo = ({ labResults }: LabInfoProps): React.JSX.Element => {
- const renderLabInfo = () => {
+export const LabInfo = ({ labResults }: LabInfoProps) => {
+ const renderHtmlLabResults = () => {
return (
- <>
- {labResults.map((labResult, labIndex) => {
- // This is to build the selector based off if orgId exists
- // Sometimes it doesn't, so we default to the base class
- // the orgId makes it so that when you have multiple, it can distinguish
- // which org it is modifying
- const accordionSelectorClass = labResult.organizationId
- ? `.accordion_${labResult.organizationId}`
- : ".accordion-rr";
- const buttonSelectorClass = labResult.organizationId
- ? `.acc_item_${labResult.organizationId}`
- : "h5";
- const labName = `Lab Results from ${
- labResult?.organizationDisplayDataProps?.[0]?.value ||
- "Unknown Organization"
- }`;
- return (
-
-
{labName}
-
- {labResult?.organizationDisplayDataProps?.map(
- (item: DisplayDataProps, index: any) => {
- if (item.value)
- return ;
- },
- )}
-
-
- .usa-accordion__button`}
- accordionSelector={`${accordionSelectorClass} > .usa-accordion__content`}
- expandButtonText={"Expand all labs"}
- collapseButtonText={"Collapse all labs"}
- />
-
-
- {labResult.diagnosticReportDataElements}
-
-
- );
- })}
- >
+
+
Lab Results
+
+
+
+
+
+
);
};
+ const renderLabResultDetails = () =>
+ (labResults as LabReportElementData[]).map((labResult, labIndex) => {
+ // This is to build the selector based off if orgId exists
+ // Sometimes it doesn't, so we default to the base class
+ // the orgId makes it so that when you have multiple, it can distinguish
+ // which org it is modifying
+ const accordionSelectorClass = labResult.organizationId
+ ? `.accordion_${labResult.organizationId}`
+ : ".accordion-rr";
+ const buttonSelectorClass = labResult.organizationId
+ ? `.acc_item_${labResult.organizationId}`
+ : "h5";
+ const labName = `Lab Results from ${
+ labResult?.organizationDisplayDataProps?.[0]?.value ||
+ "Unknown Organization"
+ }`;
+ return (
+
+
{labName}
+
+ {labResult?.organizationDisplayDataProps?.map(
+ (item: DisplayDataProps, index: any) => {
+ if (item.value) return ;
+ },
+ )}
+
+
+ .usa-accordion__button`}
+ accordionSelector={`${accordionSelectorClass} > .usa-accordion__content`}
+ expandButtonText={"Expand all labs"}
+ collapseButtonText={"Collapse all labs"}
+ />
+
+
+ {labResult.diagnosticReportDataElements}
+
+
+ );
+ });
+
return (
- {labResults.length > 0 && renderLabInfo()}
+ {labResults &&
+ (isLabReportElementDataList(labResults)
+ ? renderLabResultDetails()
+ : renderHtmlLabResults())}
);
};
diff --git a/containers/ecr-viewer/src/app/view-data/components/common.tsx b/containers/ecr-viewer/src/app/view-data/components/common.tsx
index ccaf6dd1a..f4fce1e9b 100644
--- a/containers/ecr-viewer/src/app/view-data/components/common.tsx
+++ b/containers/ecr-viewer/src/app/view-data/components/common.tsx
@@ -46,6 +46,9 @@ import {
AdministeredMedication,
AdministeredMedicationTableData,
} from "@/app/view-data/components/AdministeredMedication";
+import { Path } from "fhirpath";
+import classNames from "classnames";
+import { Fragment } from "react";
/**
* Returns a table displaying care team information.
@@ -247,41 +250,53 @@ export const returnProblemsTable = (
};
/**
- * Returns a header and tables displaying pending and future results information.
- * @param fhirBundle - The FHIR bundle containing care team data.
- * @param mappings - The object containing the fhir paths.
+ * Returns a header and tables from XHTML in the FHIR data.
+ * @param fhirBundle - The FHIR bundle.
+ * @param mapping - The fhir path.
+ * @param title - The table header title
+ * @param outerBorder - Determines whether to include an outer border for the table. Default is true.
+ * @param className - Classnames to be applied to table.
* @returns The JSX element representing the table, or undefined if no pending results are found.
*/
-export const returnPlanOfTreatmentContent = (
+export const returnHtmlTableContent = (
fhirBundle: Bundle,
- mappings: PathMappings,
+ mapping: string | Path,
+ title: string,
+ outerBorder = true,
+ className = "",
) => {
- const bundle = evaluateValue(fhirBundle, mappings["planOfTreatment"]);
+ const bundle = evaluateValue(fhirBundle, mapping);
const rawTables = formatTablesToJSON(bundle);
const tables = rawTables
- .map((rawTable) => returnPlanOfTreatmentTable(rawTable))
+ .map((rawTable) => returnTableFromJson(rawTable, outerBorder, className))
.filter((t) => !!t);
if (tables.length > 0) {
return (
- <>
- Plan of Treatment
+
+ {!!title && {title}
}
{tables}
- >
+
);
}
};
/**
- * Returns a table displaying plan of treatment information.
- * @param rawTable - A table found in the plan of treatment fhir data.
+ * Returns a table built from JSON representation of the XHTML in the FHIR data.
+ * @param rawTable - A table found in the fhir data.
+ * @param outerBorder - Determines whether to include an outer border for the table. Default is true.
+ * @param className - Classnames to be applied to table.
* @returns The JSX element representing the table, or undefined if no matching results are found.
*/
-export const returnPlanOfTreatmentTable = (rawTable: TableJson) => {
+export const returnTableFromJson = (
+ rawTable: TableJson,
+ outerBorder = true,
+ className = "",
+) => {
const { resultName, tables } = rawTable;
const flatTables = tables?.flatMap((a) => a) ?? [];
if (flatTables.length > 0) {
- const treatmentDetailHeaders = BuildHeaders(
+ const headers = BuildHeaders(
Object.keys(flatTables[0]).map((columnName) => {
return { columnName, className: "bg-gray-5 minw-10" };
}),
@@ -299,12 +314,16 @@ export const returnPlanOfTreatmentTable = (rawTable: TableJson) => {
return (
);
}
@@ -547,7 +566,11 @@ export const evaluateClinicalData = (
},
{
title: "Plan of Treatment",
- value: returnPlanOfTreatmentContent(fhirBundle, mappings),
+ value: returnHtmlTableContent(
+ fhirBundle,
+ mappings["planOfTreatment"],
+ "Plan of Treatment",
+ ),
},
{
title: "Administered Medications",
diff --git a/containers/ecr-viewer/src/styles/custom-styles.scss b/containers/ecr-viewer/src/styles/custom-styles.scss
index e9a83dd36..3e9e2b4ba 100644
--- a/containers/ecr-viewer/src/styles/custom-styles.scss
+++ b/containers/ecr-viewer/src/styles/custom-styles.scss
@@ -143,6 +143,11 @@ h4 {
margin-bottom: 1rem;
border-bottom: #1b1b1b 1px solid;
+ &.lab-results-table-from-div {
+ border-bottom: none;
+ margin-bottom: 0;
+ }
+
thead th {
background-color: #F0F0F0;
}