diff --git a/containers/ecr-viewer/src/app/components/BaseFilter.tsx b/containers/ecr-viewer/src/app/components/BaseFilter.tsx index 0fdcf0957..a90955528 100644 --- a/containers/ecr-viewer/src/app/components/BaseFilter.tsx +++ b/containers/ecr-viewer/src/app/components/BaseFilter.tsx @@ -6,9 +6,14 @@ import React, { useEffect, useRef, } from "react"; -import { Button } from "@trussworks/react-uswds"; +import { Button, Label } from "@trussworks/react-uswds"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; -import { FILTER_CLOSED, FILTER_SUBMITTED, FilterOpenContext } from "./Filters"; +import { + FILTER_CLOSED, + FILTER_SUBMITTED, + FilterOpenContext, +} from "@/app/components/Filters"; +import { toKebabCase } from "@/app/services/formatService"; /** * Custom hook to manage query parameters in the URL (set, delete, and update). Hook always resets page back to 1. @@ -24,8 +29,7 @@ export const useQueryParam = () => { // Set a query param with a specific value const setQueryParam = useCallback( - (key: string, value: string) => { - const params = new URLSearchParams(searchParams.toString()); + (params: URLSearchParams, key: string, value: string) => { params.set("page", "1"); params.set(key, value); return params; @@ -35,10 +39,9 @@ export const useQueryParam = () => { // Delete a query param or a specific query param value const deleteQueryParam = useCallback( - (name: string, value?: string) => { - const params = new URLSearchParams(searchParams.toString()); + (params: URLSearchParams, key: string) => { params.set("page", "1"); - params.delete(name, value); + params.delete(key); return params; }, [searchParams], @@ -52,9 +55,10 @@ export const useQueryParam = () => { // Update a specific query param (set or delete if default) const updateQueryParam = ( + params: URLSearchParams, key: string, value: string | { [key: string]: boolean }, - isDefault: boolean, + isDefault: boolean = false, ) => { if (typeof value === "object") { value = Object.keys(value) @@ -62,14 +66,13 @@ export const useQueryParam = () => { .join("|"); } const updatedParams = isDefault - ? deleteQueryParam(key) - : setQueryParam(key, value); + ? deleteQueryParam(params, key) + : setQueryParam(params, key, value); - // Update query params - pushQueryUpdate(updatedParams, [key]); + return updatedParams; }; - return { searchParams, updateQueryParam, pushQueryUpdate }; + return { searchParams, deleteQueryParam, updateQueryParam, pushQueryUpdate }; }; /** @@ -210,7 +213,6 @@ const FilterLegend = ({ type }: { type: string }) => { const ApplyFilterButton = ({ type }: { type: string }) => { return (
-
); }; + +/** + * A reusable radio button component, used for the filter by date feature. + * @param props - The properties for the RadioDateOption component. + * @param props.groupName - The name of the radio button group. + * @param props.option - The value of the radio option. + * @param props.label - The label to display next to the radio button. + * @param props.onChange - The callback function to handle the `onChange` event when the radio button is clicked. + * @param props.isChecked - Determines if the radio button is selected based on the current state. + * @param props.classNames - (Optional) Additional custom CSS class names to apply to the radio button wrapper. + * @returns The rendered RadioDateOption component. + */ +export const RadioDateOption = ({ + groupName, + option, + label, + onChange, + isChecked, + classNames, +}: { + groupName: string; + option: string; + label: string; + onChange: (value: string) => void; + isChecked: boolean; + classNames?: string; +}) => { + return ( +
+ onChange(e.target.value)} + checked={isChecked} + /> + +
+ ); +}; + +/** + * A group of radio button components, given a set of options. + * @param props - The properties for the RadioDateOption component. + * @param props.groupName - The name of the radio button group. + * @param props.optionsMap - A map with each option as the key, and the corresponding labels as the value. + * @param props.onChange - The callback function to handle the `onChange` event when the radio button is clicked. + * @param props.currentOption - The option currently selected. + * @param props.classNames - (Optional) Additional custom CSS class names to apply to the radio button wrapper. + * @returns The rendered RadioDateOption component. + */ +export const RadioDateOptions = ({ + groupName, + optionsMap, + onChange, + currentOption, + classNames, +}: { + groupName: string; + optionsMap: Record; + onChange: (value: string) => void; + currentOption: string; + classNames?: string; +}) => { + return ( + <> + {Object.entries(optionsMap).map(([option, label]) => ( + + ))} + + ); +}; + +/** + * A custom date input component for selecting a date. + * @param props - The properties for the CustomDateInput component. + * @param props.label - The label of the custom date input component. + * @param props.onDateChange - The function that is caulled when the date changes. + * @param props.defaultValue - The default value of the date input. + * @param props.isRequired - Boolean indicating whether or not the date is required. + * @returns A JSX element containing a date input field and corresponding label. + */ +export const CustomDateInput = ({ + label, + onDateChange, + defaultValue, + isRequired, +}: { + label: string; + onDateChange: (date: string) => void; + defaultValue: string; + isRequired: boolean; +}) => { + const today = new Date().toLocaleDateString("en-CA"); + const id = toKebabCase(label); + return ( +
+ + { + const date = e.target.value; + onDateChange(date); + }} + /> +
+ ); +}; diff --git a/containers/ecr-viewer/src/app/components/EcrTable.tsx b/containers/ecr-viewer/src/app/components/EcrTable.tsx index ea728632d..7091cbc61 100644 --- a/containers/ecr-viewer/src/app/components/EcrTable.tsx +++ b/containers/ecr-viewer/src/app/components/EcrTable.tsx @@ -10,7 +10,7 @@ import { DateRangePeriod } from "@/app/view-data/utils/date-utils"; * @param props.itemsPerPage - The number of items to be displayed in the table * @param props.sortColumn - The column to sort by * @param props.sortDirection - The direction to sort by - * @param props.filterDate - The date range used to filter data + * @param props.filterDates - The date range used to filter data * @param props.searchTerm - The search term used to list data * @param props.filterConditions - (Optional) The reportable condition(s) used to filter the data * @returns - eCR Table element @@ -22,13 +22,13 @@ const EcrTable = async ({ sortDirection, searchTerm, filterConditions, - filterDate, + filterDates, }: { currentPage: number; itemsPerPage: number; sortColumn: string; sortDirection: string; - filterDate: DateRangePeriod; + filterDates: DateRangePeriod; searchTerm?: string; filterConditions?: string[]; }) => { @@ -39,7 +39,7 @@ const EcrTable = async ({ itemsPerPage, sortColumn, sortDirection, - filterDate, + filterDates, searchTerm, filterConditions, ); diff --git a/containers/ecr-viewer/src/app/components/Filters.tsx b/containers/ecr-viewer/src/app/components/Filters.tsx index c5a220f1e..424431381 100644 --- a/containers/ecr-viewer/src/app/components/Filters.tsx +++ b/containers/ecr-viewer/src/app/components/Filters.tsx @@ -8,12 +8,25 @@ import React, { useState, } from "react"; import { Button, Icon } from "@trussworks/react-uswds"; -import { useQueryParam, Filter } from "./BaseFilter"; +import { + useQueryParam, + Filter, + RadioDateOption, + RadioDateOptions, + CustomDateInput, +} from "@/app/components/BaseFilter"; import { DEFAULT_DATE_RANGE, DateRangeOptions, dateRangeLabels, } from "@/app/view-data/utils/date-utils"; +import { formatDateTime } from "@/app/services/formatService"; + +enum ParamName { + Condition = "condition", + DateRange = "dateRange", + Dates = "dates", +} // We use a context to communicate between the overall component // and the `` component to avoid prop drilling @@ -77,7 +90,7 @@ const Filters = () => { } }, [filterBoxOpen]); - const paramKeys = ["condition", "dateRange"]; + const paramKeys = Object.values(ParamName); const resetToDefault = () => { const params = new URLSearchParams(searchParams.toString()); params.set("page", "1"); @@ -124,7 +137,8 @@ const Filters = () => { * - Updates the browser's query string when the filter is applied. */ const FilterReportableConditions = () => { - const { searchParams, updateQueryParam } = useQueryParam(); + const { searchParams, updateQueryParam, pushQueryUpdate } = useQueryParam(); + const params = new URLSearchParams(searchParams.toString()); const [filterConditions, setFilterConditions] = useState<{ [key: string]: boolean; }>({}); @@ -174,7 +188,7 @@ const FilterReportableConditions = () => { ); const resetFilterConditions = (conditions: string[]) => { - const conditionParam = searchParams.get("condition"); + const conditionParam = searchParams.get(ParamName.Condition); const conditionsToTrue = new Set(conditionParam?.split("|") || []); const conditionValue = (c: string) => { @@ -211,9 +225,15 @@ const FilterReportableConditions = () => { (key) => filterConditions[key] === true, ).length || "0" } - submitHandler={() => - updateQueryParam("condition", filterConditions, isAllSelected) - } + submitHandler={() => { + const updatedParams = updateQueryParam( + params, + ParamName.Condition, + filterConditions, + isAllSelected, + ); + pushQueryUpdate(updatedParams, [ParamName.Condition]); + }} > {/* Select All checkbox */}
@@ -260,6 +280,7 @@ const FilterReportableConditions = () => { ))}
+
); }; @@ -271,67 +292,124 @@ const FilterReportableConditions = () => { * - Updates the browser's query string when the filter is applied. */ const FilterByDate = () => { - const { searchParams, updateQueryParam } = useQueryParam(); - + const today = new Date().toLocaleDateString("en-CA"); + const { searchParams, deleteQueryParam, updateQueryParam, pushQueryUpdate } = + useQueryParam(); const [filterDateOption, setFilterDateOption] = useState(DEFAULT_DATE_RANGE); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); const isFilterDateDefault = filterDateOption === DEFAULT_DATE_RANGE; - const handleDateOptionChange = ( - event: React.ChangeEvent, - ) => { - // Set option - const { value } = event.target; - setFilterDateOption(value); - }; - const resetFilterDate = () => { - const queryDateRange = searchParams.get("dateRange"); + const queryDateRange = searchParams.get(ParamName.DateRange); if (!queryDateRange) { setFilterDateOption(DEFAULT_DATE_RANGE); + setStartDate(""); + setEndDate(""); } else if (queryDateRange !== filterDateOption) { setFilterDateOption(queryDateRange); } + if (queryDateRange === "custom") { + const queryCustomDates = searchParams.get(ParamName.Dates) || ""; + const [startDate, endDate] = queryCustomDates?.split("|"); + setStartDate(startDate); + setEndDate(endDate); + } }; // on mount, make sure the filters match the search params useEffect(resetFilterDate, []); + const submitHandler = () => { + const params = new URLSearchParams(searchParams.toString()); + if (filterDateOption === "custom") { + // TODO ANGELA: Temp: Placeholder errorhandling for when Start Date > End Date + if (new Date(startDate) > new Date(endDate)) { + alert( + "Invalid date range. Start date must be earlier or equal to the end date.", + ); + resetFilterDate(); + return; + } + if (!endDate) { + setEndDate(today); + } + const datesParam = `${startDate}|${endDate || today}`; + let updatedParams: URLSearchParams; + updatedParams = updateQueryParam( + params, + ParamName.DateRange, + filterDateOption, + isFilterDateDefault, + ); + updatedParams = updateQueryParam( + updatedParams, + ParamName.Dates, + datesParam, + ); + pushQueryUpdate(updatedParams, [ParamName.DateRange, ParamName.Dates]); + } else { + let updatedParams = updateQueryParam( + params, + ParamName.DateRange, + filterDateOption, + isFilterDateDefault, + ); + updatedParams = deleteQueryParam(updatedParams, ParamName.Dates); + pushQueryUpdate(updatedParams, [ParamName.DateRange, ParamName.Dates]); + setStartDate(""); + setEndDate(""); + } + }; + return ( - updateQueryParam("dateRange", filterDateOption, isFilterDateDefault) + title={ + filterDateOption === "custom" + ? startDate && endDate + ? `From ${formatDateTime(startDate)} to ${formatDateTime(endDate)}` + : "" + : dateRangeLabels[filterDateOption as DateRangeOptions] || "" } + submitHandler={submitHandler} >
- {Object.values(DateRangeOptions).map((option) => ( -
- +
+ + {filterDateOption === "custom" && ( +
+ + -
- ))} -
+ )}
); diff --git a/containers/ecr-viewer/src/app/page.tsx b/containers/ecr-viewer/src/app/page.tsx index 12ad62b15..32ee759fc 100644 --- a/containers/ecr-viewer/src/app/page.tsx +++ b/containers/ecr-viewer/src/app/page.tsx @@ -8,6 +8,7 @@ import NotFound from "./not-found"; import Filters from "@/app/components/Filters"; import { EcrTableLoading } from "./components/EcrTableClient"; import { + buildCustomDateRange, convertDateOptionToDateRange, DEFAULT_DATE_RANGE, } from "@/app/view-data/utils/date-utils"; @@ -34,14 +35,25 @@ const HomePage = async ({ const searchTerm = searchParams?.search as string | undefined; const filterConditions = searchParams?.condition as string | undefined; const filterConditionsArr = filterConditions?.split("|"); - const filterDateRange = convertDateOptionToDateRange( - (searchParams?.dateRange as string) || DEFAULT_DATE_RANGE, - ); + const filterDateRange = + (searchParams?.dateRange as string) || DEFAULT_DATE_RANGE; + let filterDates; + + if (filterDateRange === "custom") { + const datesParam = searchParams?.dates as string | undefined; + if (datesParam) { + filterDates = buildCustomDateRange(datesParam); + } else { + filterDates = convertDateOptionToDateRange(DEFAULT_DATE_RANGE); // TODO ANGELA: Change Default for invalid or missing dates + } + } else { + filterDates = convertDateOptionToDateRange(filterDateRange); + } let totalCount: number = 0; if (isNonIntegratedViewer) { totalCount = await getTotalEcrCount( - filterDateRange, + filterDates, searchTerm, filterConditionsArr, ); @@ -68,7 +80,7 @@ const HomePage = async ({ sortDirection={sortDirection} searchTerm={searchTerm} filterConditions={filterConditionsArr} - filterDate={filterDateRange} + filterDates={filterDates} /> diff --git a/containers/ecr-viewer/src/app/services/listEcrDataService.ts b/containers/ecr-viewer/src/app/services/listEcrDataService.ts index bd3445a60..e56c69662 100644 --- a/containers/ecr-viewer/src/app/services/listEcrDataService.ts +++ b/containers/ecr-viewer/src/app/services/listEcrDataService.ts @@ -45,7 +45,7 @@ export interface EcrDisplay { * @param itemsPerPage - The number of items to fetch * @param sortColumn - The column to sort by * @param sortDirection - The direction to sort by - * @param filterDate - The date (range) to filter on + * @param filterDates - The date (range) to filter on * @param searchTerm - The search term to use * @param filterConditions - The condition(s) to filter on * @returns A promise resolving to a list of eCR metadata @@ -55,7 +55,7 @@ export async function listEcrData( itemsPerPage: number, sortColumn: string, sortDirection: string, - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, searchTerm?: string, filterConditions?: string[], ): Promise { @@ -68,7 +68,7 @@ export async function listEcrData( itemsPerPage, sortColumn, sortDirection, - filterDate, + filterDates, searchTerm, filterConditions, ); @@ -78,7 +78,7 @@ export async function listEcrData( itemsPerPage, sortColumn, sortDirection, - filterDate, + filterDates, searchTerm, filterConditions, ); @@ -92,7 +92,7 @@ async function listEcrDataPostgres( itemsPerPage: number, sortColumn: string, sortDirection: string, - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, searchTerm?: string, filterConditions?: string[], ): Promise { @@ -101,7 +101,7 @@ async function listEcrDataPostgres( "SELECT ed.eICR_ID, ed.patient_name_first, ed.patient_name_last, ed.patient_birth_date, ed.date_created, ed.report_date, ed.report_date, ARRAY_AGG(DISTINCT erc.condition) AS conditions, ARRAY_AGG(DISTINCT ers.rule_summary) AS rule_summaries FROM ecr_data ed LEFT JOIN ecr_rr_conditions erc ON ed.eICR_ID = erc.eICR_ID LEFT JOIN ecr_rr_rule_summaries ers ON erc.uuid = ers.ecr_rr_conditions_id WHERE $[whereClause] GROUP BY ed.eICR_ID, ed.patient_name_first, ed.patient_name_last, ed.patient_birth_date, ed.date_created, ed.report_date $[sortStatement] OFFSET $[startIndex] ROWS FETCH NEXT $[itemsPerPage] ROWS ONLY", { whereClause: generateWhereStatementPostgres( - filterDate, + filterDates, searchTerm, filterConditions, ), @@ -119,7 +119,7 @@ async function listEcrDataSqlserver( itemsPerPage: number, sortColumn: string, sortDirection: string, - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, searchTerm?: string, filterConditions?: string[], ): Promise { @@ -135,7 +135,7 @@ async function listEcrDataSqlserver( sortDirection, ); const whereStatement = generateWhereStatementSqlServer( - filterDate, + filterDates, searchTerm, filterConditions, ); @@ -209,13 +209,13 @@ const processExtendedMetadata = ( /** * Retrieves the total number of eCRs stored in the ecr_data table. - * @param filterDate - The date (range) to filter on + * @param filterDates - The date (range) to filter on * @param searchTerm - The search term used to filter the count query * @param filterConditions - The array of reportable conditions used to filter the count query * @returns A promise resolving to the total number of eCRs. */ export const getTotalEcrCount = async ( - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, searchTerm?: string, filterConditions?: string[], ): Promise => { @@ -223,10 +223,14 @@ export const getTotalEcrCount = async ( switch (DATABASE_TYPE) { case "postgres": - return getTotalEcrCountPostgres(filterDate, searchTerm, filterConditions); + return getTotalEcrCountPostgres( + filterDates, + searchTerm, + filterConditions, + ); case "sqlserver": return getTotalEcrCountSqlServer( - filterDate, + filterDates, searchTerm, filterConditions, ); @@ -236,7 +240,7 @@ export const getTotalEcrCount = async ( }; const getTotalEcrCountPostgres = async ( - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, searchTerm?: string, filterConditions?: string[], ): Promise => { @@ -245,7 +249,7 @@ const getTotalEcrCountPostgres = async ( "SELECT count(DISTINCT ed.eICR_ID) FROM ecr_data as ed LEFT JOIN ecr_rr_conditions erc on ed.eICR_ID = erc.eICR_ID WHERE $[whereClause]", { whereClause: generateWhereStatementPostgres( - filterDate, + filterDates, searchTerm, filterConditions, ), @@ -255,7 +259,7 @@ const getTotalEcrCountPostgres = async ( }; const getTotalEcrCountSqlServer = async ( - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, searchTerm?: string, filterConditions?: string[], ): Promise => { @@ -263,7 +267,7 @@ const getTotalEcrCountSqlServer = async ( try { const whereStatement = generateWhereStatementSqlServer( - filterDate, + filterDates, searchTerm, filterConditions, ); @@ -281,13 +285,13 @@ const getTotalEcrCountSqlServer = async ( /** * A custom type format for where statement - * @param filterDate - The date (range) to filter on + * @param filterDates - The date (range) to filter on * @param searchTerm - Optional search term used to filter * @param filterConditions - Optional array of reportable conditions used to filter * @returns custom type format object for use by pg-promise */ export const generateWhereStatementPostgres = ( - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, searchTerm?: string, filterConditions?: string[], ) => ({ @@ -298,7 +302,7 @@ export const generateWhereStatementPostgres = ( ? generateFilterConditionsStatement(filterConditions).toPostgres() : "NULL IS NULL"; const statementDate = - generateFilterDateStatementPostgres(filterDate).toPostgres(); + generateFilterDateStatementPostgres(filterDates).toPostgres(); return `(${statementSearch}) AND (${statementDate}) AND (${statementConditions})`; }, @@ -306,13 +310,13 @@ export const generateWhereStatementPostgres = ( /** * Generate where statement for SQL Server - * @param filterDate - The date (range) to filter on + * @param filterDates - The date (range) to filter on * @param searchTerm - Optional search term used to filter * @param filterConditions - Optional array of reportable conditions used to filter * @returns - where statement for SQL Server */ const generateWhereStatementSqlServer = ( - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, searchTerm?: string, filterConditions?: string[], ) => { @@ -320,7 +324,7 @@ const generateWhereStatementSqlServer = ( const statementConditions = filterConditions ? generateFilterConditionsStatementSqlServer(filterConditions) : "NULL IS NULL"; - const statementDate = generateFilterDateStatementSqlServer(filterDate); + const statementDate = generateFilterDateStatementSqlServer(filterDates); return `(${statementSearch}) AND (${statementDate}) AND (${statementConditions})`; }; @@ -414,11 +418,11 @@ const generateFilterConditionsStatementSqlServer = ( /** * A custom type format for statement filtering by date range - * @param filterDate - Date range to filter on + * @param filterDates - Date range to filter on * @returns custom type format object for use by pg-promise */ export const generateFilterDateStatementPostgres = ( - filterDate: DateRangePeriod, + filterDates: DateRangePeriod, ) => ({ rawType: true, toPostgres: () => { @@ -426,19 +430,19 @@ export const generateFilterDateStatementPostgres = ( return [ pgPromise.as.format("ed.date_created >= $[startDate]", { - startDate: filterDate.startDate, + startDate: filterDates.startDate, }), pgPromise.as.format("ed.date_created <= $[endDate]", { - endDate: filterDate.endDate, + endDate: filterDates.endDate, }), ].join(" AND "); }, }); -const generateFilterDateStatementSqlServer = (filterDate: DateRangePeriod) => { +const generateFilterDateStatementSqlServer = (filterDates: DateRangePeriod) => { return [ - `ed.date_created >= '${filterDate.startDate.toISOString()}'`, - `ed.date_created <= '${filterDate.endDate.toISOString()}'`, + `ed.date_created >= '${filterDates.startDate.toISOString()}'`, + `ed.date_created <= '${filterDates.endDate.toISOString()}'`, ].join(" AND "); }; diff --git a/containers/ecr-viewer/src/app/tests/components/EcrTable.test.tsx b/containers/ecr-viewer/src/app/tests/components/EcrTable.test.tsx index 96112e2a3..f64d27c8b 100644 --- a/containers/ecr-viewer/src/app/tests/components/EcrTable.test.tsx +++ b/containers/ecr-viewer/src/app/tests/components/EcrTable.test.tsx @@ -49,7 +49,7 @@ describe("EcrTable", () => { itemsPerPage: 25, sortColumn: "date_created", sortDirection: "DESC", - filterDate: mockDateRange, + filterDates: mockDateRange, }), ); expect(container).toMatchSnapshot(); @@ -63,7 +63,7 @@ describe("EcrTable", () => { itemsPerPage: 25, sortColumn: "date_created", sortDirection: "DESC", - filterDate: mockDateRange, + filterDates: mockDateRange, }), ); await act(async () => { @@ -80,7 +80,7 @@ describe("EcrTable", () => { itemsPerPage: 25, sortColumn: "date_created", sortDirection: "DESC", - filterDate: mockDateRange, + filterDates: mockDateRange, searchTerm: "blah", filterConditions: ["Anthrax (disorder)"], }), diff --git a/containers/ecr-viewer/src/app/tests/components/Filters.test.tsx b/containers/ecr-viewer/src/app/tests/components/Filters.test.tsx index 51155df07..1fcc05d93 100644 --- a/containers/ecr-viewer/src/app/tests/components/Filters.test.tsx +++ b/containers/ecr-viewer/src/app/tests/components/Filters.test.tsx @@ -455,6 +455,147 @@ describe("Filter by Date Component", () => { }); }); +describe("Filter by Date Component - custom dates", () => { + it("Renders correctly after opening Filter by Date box and clicking Custom date range option", async () => { + const mockDate = new Date("2025-01-09T13:00:00"); + jest + .spyOn(global, "Date") + .mockImplementation(() => mockDate as unknown as Date); + + const { container } = render(); + + const toggleFilterButton = screen.getByRole("button", { + name: /Filter by Received Date/i, + }); + fireEvent.click(toggleFilterButton); + + await waitFor(() => + screen.getByRole("radio", { + name: "Custom date range", + }), + ); + const radio = screen.getByRole("radio", { + name: "Custom date range", + }); + fireEvent.click(radio); + + expect(container).toMatchSnapshot(); + + jest.restoreAllMocks(); + }); + it("Display start and end date fields when 'Custom date range' is selected", async () => { + const mockPush = jest.fn(); + (useRouter as jest.Mock).mockReturnValue({ push: mockPush }); + + render(); + const toggleButton = screen.getByRole("button", { + name: /Filter by Received Date/i, + }); + fireEvent.click(toggleButton); + + await waitFor(() => + screen.getByRole("radio", { + name: "Custom date range", + }), + ); + const radio = screen.getByRole("radio", { + name: "Custom date range", + }); + fireEvent.click(radio); + + expect(screen.getByText("Start date")).toBeInTheDocument(); + expect(screen.getByText("End date")).toBeInTheDocument(); + }); + it("Navigates with the correct query string on applying custom dates", async () => { + const mockPush = jest.fn(); + (useRouter as jest.Mock).mockReturnValue({ push: mockPush }); + + render(); + const toggleButton = screen.getByRole("button", { + name: /Filter by Received Date/i, + }); + fireEvent.click(toggleButton); + + await waitFor(() => + screen.getByRole("radio", { + name: "Custom date range", + }), + ); + const radio = screen.getByRole("radio", { + name: "Custom date range", + }); + fireEvent.click(radio); + + const startDateInput = screen.getByTestId("start-date"); + const endDateInput = screen.getByTestId("end-date"); + + fireEvent.change(startDateInput, { target: { value: "2025-01-01" } }); + fireEvent.change(endDateInput, { target: { value: "2025-01-02" } }); + + const applyButton = screen.getByRole("button", { name: /Apply Filter/i }); + fireEvent.click(applyButton); + + expect(toggleButton).toHaveFocus(); + + // Filter by Date button title should include custom date range + expect( + screen.getByRole("button", { + name: /Filter by Received Date/i, + }), + ).toHaveTextContent("From 01/01/2025 to 01/02/2025"); + + // Should have custom date range in search param + expect(mockPush).toHaveBeenCalledWith( + expect.stringContaining("dateRange=custom"), + ); + expect(mockPush).toHaveBeenCalledWith( + expect.stringContaining("dates=2025-01-01%7C2025-01-02"), + ); + }); + it("If no end date is given, end date defaults to today", async () => { + const mockDateString = "2025-01-09"; + const mockDate = new Date("2025-01-09T13:00:00"); + jest + .spyOn(global, "Date") + .mockImplementation(() => mockDate as unknown as Date); + + const mockPush = jest.fn(); + (useRouter as jest.Mock).mockReturnValue({ push: mockPush }); + + render(); + const toggleButton = screen.getByRole("button", { + name: /Filter by Received Date/i, + }); + fireEvent.click(toggleButton); + + await waitFor(() => + screen.getByRole("radio", { + name: "Custom date range", + }), + ); + const radio = screen.getByRole("radio", { + name: "Custom date range", + }); + fireEvent.click(radio); + + const startDateInput = screen.getByTestId("start-date"); + fireEvent.change(startDateInput, { target: { value: "2025-01-01" } }); + + const applyButton = screen.getByRole("button", { + name: /Apply Filter/i, + }); + fireEvent.click(applyButton); + + // Should have custom date range in search param + expect(mockPush).toHaveBeenCalledWith( + expect.stringContaining("dateRange=custom"), + ); + expect(mockPush).toHaveBeenCalledWith( + expect.stringContaining(`dates=2025-01-01%7C${mockDateString}`), + ); + }); +}); + describe("Filter Opening/Closing Controls", () => { beforeEach(() => { jest.clearAllMocks(); @@ -669,7 +810,9 @@ describe("Reset button", () => { fireEvent.click(radio); expect(radio).toBeChecked(); - fireEvent.submit(radio); + const applyButton = screen.getByRole("button", { name: /Apply Filter/i }); + fireEvent.click(applyButton); + await waitFor(() => screen.findByText("Last 7 days")); // should be closed diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Filters.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Filters.test.tsx.snap index f19680ceb..911f0e6bf 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Filters.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Filters.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Filter by Date Component Renders correctly after opening Filter by Date box 1`] = ` +exports[`Filter by Date Component - custom dates Renders correctly after opening Filter by Date box and clicking Custom date range option 1`] = `
- Last year + Received Date
@@ -84,12 +84,12 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -101,12 +101,12 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -118,12 +118,12 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -135,12 +135,12 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -152,13 +152,13 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -170,15 +170,326 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+ +`; + +exports[`Filter by Date Component Renders correctly after opening Filter by Date box 1`] = ` +
+
+
+
+ + FILTERS: + +
+
+ +
+
+
+ + Filter by + Received Date + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+
+ + +
+
+
+
-