Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure list view uses FH results #94

Merged
merged 12 commits into from
Dec 5, 2022
11 changes: 11 additions & 0 deletions src/components/reports/ReportForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,17 @@ const renderTestReportForm = () => {
render(<ContextAndModal children={<ReportForm initialValues={noValues} />} />, { wrapper: BrowserRouter });
};

beforeAll(() => {
/*
* For some reason on first load of FHIR server, creating a batch of requests doesn't seem to validate
* Sending a resource before any test are run fixed the issue
*/
const practitioner = new Practitioner();
practitioner.resourceType = "Practitioner";
practitioner.identifier = [createIdentifier("initial")];
return createPractitioner(practitioner);
});

Comment on lines +168 to +178
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, is this a frontend issue I wonder? Does it work if you interact with the backend directly? Hopefully this doesn't cause any user interaction issues on the deployed application.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's only the first time a FHIR server is spun up that it happens which is interesting.

describe("Report form", () => {
beforeEach(() => {
return deleteFhirData();
Expand Down
124 changes: 103 additions & 21 deletions src/components/results-list/ResultsDataFetcher.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { createBundle } from "../../fhir/api";
import { ContextAndModal, deleteFhirData, sendBundle } from "../../fhir/testUtilities";
import { geneCoding } from "../../code_systems/hgnc";
import ResultsDataFetcher from "./ResultsDataFetcher";
import { act } from "react-dom/test-utils";
import userEvent from "@testing-library/user-event";

const reportedGenes = [geneCoding("HGNC:4389", "GNA01"), geneCoding("HGNC:6547", "LDLR")];

Expand All @@ -15,7 +17,7 @@ type OverridingFields = {
nhsNumber: string;
firstName: string;
lastName: string;
familyNumber: string;
familyNumber?: string;
};
sample: {
specimenCode: string;
Expand All @@ -24,21 +26,26 @@ type OverridingFields = {
variant: {
cDnaHgvs: string;
gene: string;
isPathogenic: boolean;
transcript?: string;
};
};

const clearFhirAndSendReports = async () => {
await deleteFhirData();

const overridingValues = [
// not FH
createPatientOverrides("Daffy", "Duck", ["R59"], "HGNC:4389", "c.115A>T"),
// not FH so shouldn't be in the list
createPatientOverrides("Daffy", "Duck", ["R59"], "HGNC:4389", "c.140G>A"),
// Has a family member who has been tests
createPatientOverrides("Bugs", "Bunny", ["R134"], "HGNC:6547", "c.113A>T", "F12345"),
createPatientOverrides("Betty", "Bunny", ["R134"], "HGNC:6547", "c.113A>T", "F12345"),
// No family member who has been tested
createPatientOverrides("Road", "Runner", ["R134"], "HGNC:6547", "c.110A>T", "F10000"),
createPatientOverrides("Wile", "Coyote", ["R134"], "HGNC:6547", "c.112A>T"),
createPatientOverrides("Bugs", "Bunny", ["R134"], "HGNC:6547", "NM_000527.5:c.259T>G", "F12345"),
createPatientOverrides("Betty", "Bunny", ["R134"], "HGNC:6547", "NM_000527.5:c.259T>G", "F12345"),
// No family member who has been tested, cascading testing required
createPatientOverrides("Road", "Runner", ["R134"], "HGNC:6547", "NM_000527.5:c.27C>T", "F10000"),
// No family member
createPatientOverrides("Wile", "Coyote", ["R134"], "HGNC:6547", "NM_000527.5:c.58G>A"),
// Benign
createPatientOverrides("Yosemite", "Sam", ["R134"], "HGNC:6547", "NM_000527.5:c.9C>T", "F54321", false),
];

for (const override of overridingValues) {
Expand All @@ -55,32 +62,37 @@ const createPatientOverrides = (
gene: string,
cDnaHgvs: string,
familyNumber?: string,
isPathogenic = true,
): OverridingFields => {
let transcript: string | undefined = undefined;
if (cDnaHgvs.includes(":")) {
transcript = cDnaHgvs.split(":")[0];
}
return {
patient: {
mrn: generateNumber("mrn"),
nhsNumber: generateNumber("nhs"),
mrn: generateId("mrn"),
nhsNumber: generateId("nhs"),
firstName: firstName,
lastName: lastName,
familyNumber: familyNumber ? familyNumber : generateNumber("family"),
familyNumber: familyNumber,
},
sample: { specimenCode: generateNumber("specimen"), reasonForTest: testReason },
sample: { specimenCode: generateId("specimen"), reasonForTest: testReason },
variant: {
cDnaHgvs: cDnaHgvs,
gene: gene,
isPathogenic: isPathogenic,
transcript: transcript,
},
};
};

const generateNumber = (type?: "nhs" | "family" | "specimen" | "mrn") => {
const generateId = (type?: "nhs" | "specimen" | "mrn") => {
let num;
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const randomCharacter = characters[Math.floor(Math.random() * characters.length)];

if (type === "nhs") {
num = Math.floor(Math.random() * Math.pow(10, 10));
} else if (type === "family") {
num = randomCharacter + Math.floor(Math.random() * Math.pow(10, 6));
} else if (type === "specimen") {
num = randomCharacter + Math.floor(Math.random() * Math.pow(10, 10));
} else if (type === "mrn") {
Expand All @@ -102,18 +114,26 @@ const changePatientInfo = (valuesToUpdate: OverridingFields) => {
newPatient.sample.reasonForTest = valuesToUpdate.sample.reasonForTest;
newPatient.variant[0].cDnaHgvs = valuesToUpdate.variant.cDnaHgvs;
newPatient.variant[0].gene = valuesToUpdate.variant.gene;
if (!valuesToUpdate.variant.isPathogenic) {
newPatient.result.reportFinding = "LA6577-6"; // negative
newPatient.variant[0].classification = "LA26334-5"; // likely benign
}
if (valuesToUpdate.variant.transcript) {
newPatient.variant[0].transcript = valuesToUpdate.variant.transcript;
}

return newPatient;
};

describe("Results table", () => {
beforeEach(() => {
return clearFhirAndSendReports();
});
beforeAll(() => {
return clearFhirAndSendReports();
});

describe("Results table", () => {
/**
* Given the FHIR API is cleared and has 5 separate reports added with one variant
* Given the FHIR API is cleared and has 6 separate reports added with one variant each (one not FH)
* When the Results list page is rendered
* Then there should be 5 variants listed
* Then there should be 5 FH variants listed
*/
test("patients are in table", async () => {
render(<ContextAndModal children={<ResultsDataFetcher />} />);
Expand All @@ -128,3 +148,65 @@ describe("Results table", () => {
);
});
});

describe("Modal from results table", () => {
beforeEach(() => {
render(<ContextAndModal children={<ResultsDataFetcher />} />);
});

const findPatientAndClickThem = async (patientRegex: RegExp) => {
const patient = await screen.findByText(patientRegex);
await act(async () => {
userEvent.click(patient);
});
};

const textIsInDocument = async (regExp: RegExp) => {
await waitFor(
() => {
expect(screen.queryByText(regExp)).toBeInTheDocument();
},
{ timeout: 15000 },
);
};

/**
* Given that there are two patients reports with the same family number and pathogenic variants
* When one of the patients is clicked
* Then the modal shows cascade testing complete
*/
test("Cascade testing has been carried out is shown", async () => {
await findPatientAndClickThem(/Betty/);
await textIsInDocument(/Cascade testing has been performed for this patient/);
});

/**
* Given that there is one patient with a family number and a pathogenic variant
* When the patient is clicked
* Then the modal shows cascade testing required
*/
test("Cascade testing required is shown", async () => {
await findPatientAndClickThem(/Runner/);
await textIsInDocument(/please offer cascade testing/);
});

/**
* Given that there is one patient with a pathogenic variant but no family number
* When the patient is clicked
* Then the modal shows no family number recorded
*/
test("No family number is shown", async () => {
await findPatientAndClickThem(/Coyote/);
await textIsInDocument(/no family number recorded/);
});

/**
* Given that there is one patient with a negative overall interpretation
* When the patient is clicked
* Then the modal shows no pathogenic variants
*/
test("Negative result is shown", async () => {
await findPatientAndClickThem(/Negative/);
await textIsInDocument(/Patient has no pathogenic variants reported/);
});
});
67 changes: 41 additions & 26 deletions src/components/results-list/ResultsDataFetcher.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
import { FC, useEffect, useState, useContext } from "react";
import { FC, useContext, useEffect, useState } from "react";
import { FhirContext } from "../fhir/FhirContext";

import LoadingSpinner from "../UI/LoadingSpinner";
import ModalWrapper from "../UI/ModalWrapper";
import { ModalState } from "../UI/ModalWrapper";
import ModalWrapper, { ModalState } from "../UI/ModalWrapper";
import ResultsList from "../results-list/ResultsList";

import { Patient, Observation } from "@smile-cdr/fhirts/dist/FHIR-R4/classes/models-r4";
import { Observation, Patient } from "@smile-cdr/fhirts/dist/FHIR-R4/classes/models-r4";

export type TrimmedObservation = { cDnaChange: string; observationId: string };
type PatientResult = {
patientId: string;
officialPatientIdentifier: string;
familyIdentifier?: string;
firstName: string;
lastName: string;
overallInterpretation: string;
observations: TrimmedObservation[];
};
export type ParsedResultsModel = PatientResult[];

const HGVS_CDNA_LOINC = "48004-6";
const FH_CODE = "R134";
const VARIANT_PROFILE = "http://hl7.org/fhir/uv/genomics-reporting/StructureDefinition/variant";
const INTERPRETATION_PROFILE = "http://hl7.org/fhir/uv/genomics-reporting/StructureDefinition/overall-interpretation";
const FAMILY_NUMBER_SYSTEM = "http://fhir.nhs.uk/Id/nhs-family-number";

function filterObservations(patientId: string, VARIANT_PROFILE: string, observations: Observation[]) {
return observations.filter((observation) => {
if (!observation.subject?.reference || !observation.meta?.profile) {
return;
}
const subjectIdLong = observation.subject.reference;
const profiles = observation.meta.profile;
return subjectIdLong.includes(patientId) && profiles.includes(VARIANT_PROFILE);
});
}

const ResultsDataFetcher: FC = () => {
const ctx = useContext(FhirContext);
Expand All @@ -27,7 +42,7 @@ const ResultsDataFetcher: FC = () => {
const [parsedResults, setParsedResults] = useState<ParsedResultsModel | null>(null);

useEffect(() => {
const observationQueryUrl = `Observation?_profile=http://hl7.org/fhir/uv/genomics-reporting/StructureDefinition/variant&_include=Observation:subject`;
const observationQueryUrl = `DiagnosticReport/?code=${FH_CODE}&_include=DiagnosticReport:result&_include=DiagnosticReport:subject`;

setIsLoading(true);

Expand Down Expand Up @@ -67,9 +82,7 @@ const ResultsDataFetcher: FC = () => {
})
.map((entry) => entry.resource as Observation);

const readableResults = createReadableResults(patients, observations);

return readableResults;
return createReadableResults(patients, observations);
};

/**
Expand All @@ -82,31 +95,26 @@ const ResultsDataFetcher: FC = () => {

// extract the required data and store in a trimmed down data structure
patients.forEach((patient) => {
if (!(patient.id && patient.identifier?.[0]?.value && patient.name?.[0]?.given && patient.name?.[0]?.family)) {
if (!(patient.id && patient.name?.[0]?.given && patient.name?.[0]?.family)) {
return;
}

const patientId = patient.id;
const officialPatientIdentifier = patient.identifier[0].value;
const familyNumber = patient.identifier?.find((id) => id.system === FAMILY_NUMBER_SYSTEM)?.value;
const firstName = patient.name[0].given[0];
const lastName = patient.name[0].family;

// return observations belonging to a patient based on the patient ID
const patientObservations = observations.filter((observation) => {
if (!observation.subject?.reference) return;

const subjectIdLong = observation.subject.reference;

return subjectIdLong.includes(patientId);
});
// return variants belonging to a patient based on the patient ID
const patientVariants = filterObservations(patientId, VARIANT_PROFILE, observations);
const overallInterpretation = filterObservations(patientId, INTERPRETATION_PROFILE, observations);

if (!patientObservations || patientObservations.length === 0) {
if (!patientVariants || patientVariants.length === 0) {
return;
}

// extract required data from each observation
let trimmedObservations: TrimmedObservation[] = [];
patientObservations.forEach((observation) => {
patientVariants.forEach((observation) => {
if (!observation?.id) {
return;
}
Expand All @@ -126,13 +134,16 @@ const ResultsDataFetcher: FC = () => {
trimmedObservations = [...trimmedObservations, { cDnaChange: cDnaChange, observationId: observation.id }];
});

const interpretation = overallInterpretation.at(0)?.valueCodeableConcept?.coding?.at(0)?.display || "Unknown";

readableResults = [
...readableResults,
{
patientId: patientId,
officialPatientIdentifier: officialPatientIdentifier,
familyIdentifier: familyNumber,
firstName: firstName,
lastName: lastName,
overallInterpretation: interpretation,
observations: trimmedObservations,
},
];
Expand All @@ -141,16 +152,20 @@ const ResultsDataFetcher: FC = () => {
return readableResults;
};

if (!parsedResults) {
return <div>Something went wrong getting observations. Please try again later.</div>;
if (!parsedResults && !isLoading) {
return <div>Familial hypercholesterolemia patient results were found on the FHIR server</div>;
}

let resultsComponent = <></>;
if (parsedResults) {
const sortedResults = parsedResults.sort((a, b) => a.lastName.localeCompare(b.lastName));
resultsComponent = <ResultsList results={sortedResults} />;
}
return (
<>
<ModalWrapper isError={modal?.isError} modalMessage={modal?.message} onClear={() => setModal(null)} />
{isLoading && <LoadingSpinner asOverlay message={"Getting observations..."} />}

<ResultsList results={parsedResults} />
{resultsComponent}
</>
);
};
Expand Down
Loading