-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
482 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { test, expect } from "@playwright/test"; | ||
import { TEST_URL } from "../playwright-setup"; | ||
import { STEP_TWO_PAGE_TITLE } from "@/app/query/components/patientSearchResults/PatientSearchResultsTable"; | ||
import { STEP_THREE_PAGE_TITLE } from "@/app/query/components/selectQuery/SelectSavedQuery"; | ||
import { TEST_PATIENT, TEST_PATIENT_NAME } from "./constants"; | ||
|
||
test.describe("alternate queries with the Query Connector", () => { | ||
test.beforeEach(async ({ page }) => { | ||
// Start every test on our main landing page | ||
await page.goto(TEST_URL); | ||
}); | ||
|
||
test("query using form-fillable demo patient by phone number", async ({ | ||
page, | ||
}) => { | ||
await page.getByRole("button", { name: "Go to the demo" }).click(); | ||
await page.getByRole("button", { name: "Fill fields" }).click(); | ||
|
||
// Delete last name and MRN to force phone number as one of the 3 fields | ||
await page.getByLabel("Last Name").clear(); | ||
await page.getByLabel("Medical Record Number").clear(); | ||
|
||
// Among verification, make sure phone number is right | ||
await page.getByRole("button", { name: "Search for patient" }).click(); | ||
await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); | ||
await expect( | ||
page.getByRole("heading", { name: STEP_TWO_PAGE_TITLE }), | ||
).toBeVisible(); | ||
await page.getByRole("link", { name: "Select patient" }).click(); | ||
await expect( | ||
page.getByRole("heading", { name: STEP_THREE_PAGE_TITLE }), | ||
).toBeVisible(); | ||
await page.getByRole("button", { name: "Submit" }).click(); | ||
await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); | ||
|
||
await expect(page.getByText("Patient Name")).toBeVisible(); | ||
await expect(page.getByText(TEST_PATIENT_NAME)).toBeVisible(); | ||
await expect(page.getByText("Contact")).toBeVisible(); | ||
await expect(page.getByText(TEST_PATIENT.Phone)).toBeVisible(); | ||
await expect(page.getByText("Patient Identifiers")).toBeVisible(); | ||
await expect(page.getByText(TEST_PATIENT.MRN)).toBeVisible(); | ||
}); | ||
|
||
test("social determinants query with generalized function", async ({ | ||
page, | ||
}) => { | ||
await page.getByRole("button", { name: "Go to the demo" }).click(); | ||
await page.getByRole("button", { name: "Fill fields" }).click(); | ||
await page.getByRole("button", { name: "Search for patient" }).click(); | ||
await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); | ||
|
||
await page.getByRole("link", { name: "Select patient" }).click(); | ||
await expect( | ||
page.getByRole("heading", { name: "Select a query" }), | ||
).toBeVisible(); | ||
await page.getByTestId("Select").selectOption("social-determinants"); | ||
await page.getByRole("button", { name: "Submit" }).click(); | ||
await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); | ||
|
||
await expect( | ||
page.getByRole("heading", { name: "Patient Record" }), | ||
).toBeVisible(); | ||
}); | ||
|
||
test("form-fillable STI query using generalized function", async ({ | ||
page, | ||
}) => { | ||
await page.getByRole("button", { name: "Go to the demo" }).click(); | ||
await page.getByRole("button", { name: "Fill fields" }).click(); | ||
await page.getByRole("button", { name: "Search for patient" }).click(); | ||
await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); | ||
await page.getByRole("link", { name: "Select patient" }).click(); | ||
await page.getByTestId("Select").selectOption("chlamydia"); | ||
await page.getByRole("button", { name: "Submit" }).click(); | ||
await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); | ||
|
||
await expect( | ||
page.getByRole("heading", { name: "Patient Record" }), | ||
).toBeVisible(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { hyperUnluckyPatient } from "@/app/constants"; | ||
|
||
export const TEST_PATIENT = hyperUnluckyPatient; | ||
export const TEST_PATIENT_NAME = | ||
hyperUnluckyPatient.FirstName + " A. " + hyperUnluckyPatient.LastName; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { test, expect } from "@playwright/test"; | ||
import { TEST_URL } from "../playwright-setup"; | ||
import { metadata } from "@/app/constants"; | ||
|
||
test("landing page loads", async ({ page }) => { | ||
await page.goto(TEST_URL); | ||
|
||
// Check that each expected text section is present | ||
await expect( | ||
page.getByRole("heading", { name: "Data collection made easier" }), | ||
).toBeVisible(); | ||
await expect( | ||
page.getByRole("heading", { name: "What is it?" }), | ||
).toBeVisible(); | ||
await expect( | ||
page.getByRole("heading", { name: "How does it work?" }), | ||
).toBeVisible(); | ||
|
||
// Check that interactable elements are present (header and Get Started) | ||
await expect(page.getByRole("link", { name: metadata.title })).toBeVisible(); | ||
await expect( | ||
page.getByRole("button", { name: "Go to the demo" }), | ||
).toBeVisible(); | ||
}); |
123 changes: 123 additions & 0 deletions
123
query-connector/src/app/query/components/SelectQuery.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
"use client"; | ||
import React, { useEffect, useState } from "react"; | ||
import { FHIR_SERVERS, USE_CASES, ValueSetItem } from "../../constants"; | ||
import CustomizeQuery from "./CustomizeQuery"; | ||
import SelectSavedQuery from "./selectQuery/SelectSavedQuery"; | ||
|
||
import { QueryResponse } from "@/app/query-service"; | ||
import { Patient } from "fhir/r4"; | ||
import { | ||
fetchQueryResponse, | ||
fetchUseCaseValueSets, | ||
} from "./selectQuery/queryHooks"; | ||
import LoadingView from "./LoadingView"; | ||
|
||
interface SelectQueryProps { | ||
goForward: () => void; | ||
goBack: () => void; | ||
selectedQuery: USE_CASES; | ||
setSelectedQuery: React.Dispatch<React.SetStateAction<USE_CASES>>; | ||
patientForQuery: Patient | undefined; | ||
resultsQueryResponse: QueryResponse; | ||
setResultsQueryResponse: React.Dispatch<React.SetStateAction<QueryResponse>>; | ||
fhirServer: FHIR_SERVERS; | ||
setFhirServer: React.Dispatch<React.SetStateAction<FHIR_SERVERS>>; | ||
setLoading: (isLoading: boolean) => void; | ||
} | ||
|
||
/** | ||
* @param root0 - SelectQueryProps | ||
* @param root0.goBack - Callback to return to previous page | ||
* @param root0.goForward - Callback to go to the next page | ||
* @param root0.selectedQuery - query we chose for further customization | ||
* @param root0.setSelectedQuery - callback function to update the selected query | ||
* @param root0.patientForQuery - patient to apply a particular query for | ||
* @param root0.resultsQueryResponse - Response of selected query | ||
* @param root0.setResultsQueryResponse - Callback function to update selected | ||
* query | ||
* @param root0.fhirServer - the FHIR server that we're running the query against | ||
* @param root0.setFhirServer - callback function to update the FHIR server | ||
* @returns - The selectQuery component. | ||
*/ | ||
const SelectQuery: React.FC<SelectQueryProps> = ({ | ||
selectedQuery, | ||
patientForQuery, | ||
resultsQueryResponse, | ||
fhirServer, | ||
goForward, | ||
goBack, | ||
setSelectedQuery, | ||
setResultsQueryResponse, | ||
setFhirServer, | ||
}) => { | ||
const [showCustomizeQuery, setShowCustomizedQuery] = useState(false); | ||
const [queryValueSets, setQueryValueSets] = useState<ValueSetItem[]>( | ||
[] as ValueSetItem[], | ||
); | ||
const [loadingQueryValueSets, setLoadingQueryValueSets] = | ||
useState<boolean>(false); | ||
|
||
const [loadingResultResponse, setLoadingResultResponse] = | ||
useState<boolean>(false); | ||
|
||
useEffect(() => { | ||
// Gate whether we actually update state after fetching so we | ||
// avoid name-change race conditions | ||
let isSubscribed = true; | ||
|
||
fetchUseCaseValueSets( | ||
selectedQuery, | ||
setQueryValueSets, | ||
isSubscribed, | ||
setLoadingQueryValueSets, | ||
).catch(console.error); | ||
|
||
// Destructor hook to prevent future state updates | ||
return () => { | ||
isSubscribed = false; | ||
}; | ||
}, [selectedQuery, setQueryValueSets]); | ||
|
||
async function onSubmit() { | ||
await fetchQueryResponse({ | ||
patientForQuery: patientForQuery, | ||
selectedQuery: selectedQuery, | ||
queryValueSets: queryValueSets, | ||
fhirServer: fhirServer, | ||
queryResponseStateCallback: setResultsQueryResponse, | ||
setIsLoading: setLoadingResultResponse, | ||
}).catch(console.error); | ||
goForward(); | ||
} | ||
|
||
const displayLoading = loadingResultResponse || loadingQueryValueSets; | ||
return ( | ||
<> | ||
{displayLoading && <LoadingView loading={loadingResultResponse} />} | ||
|
||
{showCustomizeQuery ? ( | ||
<CustomizeQuery | ||
useCaseQueryResponse={resultsQueryResponse} | ||
queryType={selectedQuery} | ||
queryValuesets={queryValueSets} | ||
setQueryValuesets={setQueryValueSets} | ||
goBack={() => setShowCustomizedQuery(false)} | ||
></CustomizeQuery> | ||
) : ( | ||
<SelectSavedQuery | ||
selectedQuery={selectedQuery} | ||
fhirServer={fhirServer} | ||
loadingQueryValueSets={loadingQueryValueSets} | ||
goBack={goBack} | ||
setSelectedQuery={setSelectedQuery} | ||
setShowCustomizedQuery={setShowCustomizedQuery} | ||
handleSubmit={onSubmit} | ||
setFhirServer={setFhirServer} | ||
></SelectSavedQuery> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export default SelectQuery; | ||
export const RETURN_TO_STEP_ONE_LABEL = "Return to Select patient"; |
141 changes: 141 additions & 0 deletions
141
query-connector/src/app/query/components/selectQuery/SelectSavedQuery.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { | ||
FHIR_SERVERS, | ||
FhirServers, | ||
USE_CASES, | ||
demoQueryOptions, | ||
} from "@/app/constants"; | ||
import { Select, Button } from "@trussworks/react-uswds"; | ||
import Backlink from "../backLink/Backlink"; | ||
import styles from "./selectQuery.module.css"; | ||
import { useState } from "react"; | ||
|
||
type SelectSavedQueryProps = { | ||
selectedQuery: string; | ||
fhirServer: FHIR_SERVERS; | ||
loadingQueryValueSets: boolean; | ||
goBack: () => void; | ||
setSelectedQuery: (selectedQuery: USE_CASES) => void; | ||
setShowCustomizedQuery: (showCustomize: boolean) => void; | ||
handleSubmit: () => void; | ||
setFhirServer: React.Dispatch<React.SetStateAction<FHIR_SERVERS>>; | ||
}; | ||
|
||
/** | ||
* Component to poulate pre-existing queries | ||
* @param root0 - params | ||
* @param root0.goBack - return function for the previous page | ||
* @param root0.selectedQuery - specified query for future customization / | ||
* application | ||
* @param root0.setSelectedQuery - callback function for specified query | ||
* @param root0.setShowCustomizedQuery - toggle to switch to customization | ||
* view | ||
* @param root0.handleSubmit - submit handler | ||
* @param root0.fhirServer - fhir server to apply a query against | ||
* @param root0.setFhirServer - function to update the fhir server | ||
* @param root0.loadingQueryValueSets - flag for whether the value sets are | ||
* still being fetched from the db | ||
* @returns SelectedSavedQuery component | ||
*/ | ||
const SelectSavedQuery: React.FC<SelectSavedQueryProps> = ({ | ||
selectedQuery, | ||
fhirServer, | ||
loadingQueryValueSets, | ||
goBack, | ||
setSelectedQuery, | ||
setShowCustomizedQuery, | ||
handleSubmit, | ||
setFhirServer, | ||
}) => { | ||
const [showAdvanced, setShowAdvanced] = useState(false); | ||
|
||
return ( | ||
<form className="content-container-smaller-width"> | ||
{/* Back button */} | ||
<Backlink onClick={goBack} label={"Return to select a patient"} /> | ||
<h1 className={`${styles.selectQueryHeaderText}`}> | ||
{STEP_THREE_PAGE_TITLE} | ||
</h1> | ||
<div | ||
className={`font-sans-md text-light ${styles.selectQueryExplanationText}`} | ||
> | ||
We will request all data related to your selected patient and query. By | ||
only showing relevant data for your query, we decrease the burden on our | ||
systems and protect patient privacy. If you would like to customize the | ||
query response, click on the "customize query" button. | ||
</div> | ||
<h3 className="margin-bottom-3">Query</h3> | ||
<div className={styles.queryRow}> | ||
{/* Select a query drop down */} | ||
<Select | ||
id="querySelect" | ||
name="query" | ||
value={selectedQuery} | ||
onChange={(e) => setSelectedQuery(e.target.value as USE_CASES)} | ||
className={`${styles.queryDropDown}`} | ||
required | ||
> | ||
{demoQueryOptions.map((option) => ( | ||
<option key={option.value} value={option.value}> | ||
{option.label} | ||
</option> | ||
))} | ||
</Select> | ||
<Button | ||
type="button" | ||
className={`usa-button--outline bg-white ${styles.customizeButton}`} | ||
onClick={() => setShowCustomizedQuery(true)} | ||
> | ||
Customize query | ||
</Button> | ||
</div> | ||
|
||
{showAdvanced && ( | ||
<div> | ||
<h3 className="margin-bottom-3">Health Care Organization (HCO)</h3> | ||
<Select | ||
id="fhir_server" | ||
name="fhir_server" | ||
value={fhirServer} | ||
onChange={(e) => setFhirServer(e.target.value as FHIR_SERVERS)} | ||
required | ||
className={`${styles.queryDropDown}`} | ||
> | ||
Select HCO | ||
{FhirServers.map((fhirServer: string) => ( | ||
<option key={fhirServer} value={fhirServer}> | ||
{fhirServer} | ||
</option> | ||
))} | ||
</Select> | ||
</div> | ||
)} | ||
|
||
{!showAdvanced && ( | ||
<div> | ||
<Button | ||
className={`usa-button--unstyled margin-left-auto ${styles.searchCallToActionButton}`} | ||
type="button" | ||
onClick={() => setShowAdvanced(true)} | ||
> | ||
Advanced... | ||
</Button> | ||
</div> | ||
)} | ||
|
||
{/* Submit Button */} | ||
<div className="padding-top-6"> | ||
<Button | ||
type="button" | ||
disabled={!selectedQuery && !loadingQueryValueSets} | ||
className={selectedQuery ? "usa-button" : "usa-button disabled"} | ||
onClick={() => handleSubmit()} | ||
> | ||
Submit | ||
</Button> | ||
</div> | ||
</form> | ||
); | ||
}; | ||
|
||
export default SelectSavedQuery; | ||
export const STEP_THREE_PAGE_TITLE = "Step 3: Select a query"; |
Oops, something went wrong.