;
+}> = ({children, initialEntries = ["/"]}) => (
+
+ {children}
+
+)
+
+const TestComponent: React.FC<{
+ onContextReady?: (context: NavigationContextType) => void;
+}> = ({onContextReady}) => {
+ const navigationContext = useNavigationContext()
+
+ React.useEffect(() => {
+ if (onContextReady) {
+ onContextReady(navigationContext)
+ }
+ }, [navigationContext, onContextReady])
+
+ return Test Component
+}
+
+describe("NavigationProvider", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ describe("useNavigationContext hook", () => {
+ it("should provide context when used within NavigationProvider", () => {
+ const {result} = renderHook(() => useNavigationContext(), {
+ wrapper: TestWrapper
+ })
+
+ expect(result.current).toBeDefined()
+ expect(typeof result.current.pushNavigation).toBe("function")
+ expect(typeof result.current.goBack).toBe("function")
+ expect(typeof result.current.getBackPath).toBe("function")
+ })
+ })
+
+ describe("pushNavigation", () => {
+ it("should add new entry and handle duplicates", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.pushNavigation("/test-path")
+ context!.pushNavigation("/test-path") // duplicate should be ignored
+ })
+
+ expect(logger.info).toHaveBeenCalledWith("Navigation: Pushing entry", {
+ path: "/test-path",
+ stackLength: expect.any(Number)
+ })
+ })
+
+ it("should handle prescription details and list pages specially", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.pushNavigation(FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT)
+ context!.pushNavigation(FRONTEND_PATHS.PRESCRIPTION_DETAILS_PAGE)
+ })
+
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.stringContaining("Navigation:"),
+ expect.any(Object)
+ )
+ })
+ })
+
+ describe("setOriginalSearchPage", () => {
+ it.each([
+ ["prescriptionId", FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID],
+ ["nhsNumber", FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER],
+ ["basicDetails", FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS]
+ ])(
+ "should set original search page for %s search",
+ async (searchType, expectedPath) => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.setOriginalSearchPage(
+ searchType as "prescriptionId" | "nhsNumber" | "basicDetails"
+ )
+ })
+
+ expect(logger.info).toHaveBeenCalledWith(
+ "Navigation: Setting original search page",
+ {path: expectedPath}
+ )
+ }
+ )
+ })
+
+ describe("captureOriginalSearchParameters", () => {
+ it("should capture parameters and clear navigation stack", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ const searchParams = {prescriptionId: "12345", issueNumber: "1"}
+
+ await act(async () => {
+ context!.captureOriginalSearchParameters(
+ "prescriptionId",
+ searchParams
+ )
+ })
+
+ expect(logger.info).toHaveBeenCalledWith(
+ "Navigation: Capturing original search parameters",
+ {searchType: "prescriptionId", params: searchParams}
+ )
+ })
+
+ it("should handle basic details search parameters", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ const searchParams = {
+ firstName: "John",
+ lastName: "Doe",
+ dobDay: "01",
+ dobMonth: "01",
+ dobYear: "1990",
+ postcode: "SW1A 1AA"
+ }
+
+ await act(async () => {
+ context!.captureOriginalSearchParameters("basicDetails", searchParams)
+ })
+
+ expect(logger.info).toHaveBeenCalledWith(
+ "Navigation: Capturing original search parameters",
+ {searchType: "basicDetails", params: searchParams}
+ )
+ })
+ })
+
+ describe("getOriginalSearchParameters", () => {
+ it("should return null when no parameters captured", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ expect(context!.getOriginalSearchParameters()).toBeNull()
+ })
+
+ it("should return captured parameters", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ const searchParams = {prescriptionId: "12345"}
+
+ await act(async () => {
+ context!.captureOriginalSearchParameters(
+ "prescriptionId",
+ searchParams
+ )
+ })
+
+ expect(context!.getOriginalSearchParameters()).toEqual(searchParams)
+ })
+ })
+
+ describe("getRelevantSearchParameters", () => {
+ it("should return empty object when no parameters or type mismatch", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.captureOriginalSearchParameters("prescriptionId", {
+ prescriptionId: "12345"
+ })
+ })
+
+ expect(context!.getRelevantSearchParameters("prescriptionId")).toEqual({
+ prescriptionId: "12345"
+ })
+ expect(context!.getRelevantSearchParameters("nhsNumber")).toEqual({})
+ })
+
+ it("should filter relevant parameters correctly", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ const allParams = {
+ prescriptionId: "12345",
+ issueNumber: "1",
+ firstName: "John",
+ nhsNumber: "1234567890",
+ postcode: "SW1A 1AA"
+ }
+
+ await act(async () => {
+ context!.captureOriginalSearchParameters("basicDetails", allParams)
+ })
+
+ const result = context!.getRelevantSearchParameters("basicDetails")
+ expect(result).toEqual({
+ firstName: "John",
+ postcode: "SW1A 1AA"
+ })
+
+ expect(logger.info).toHaveBeenCalledWith(
+ "Navigation: Getting relevant search parameters",
+ expect.objectContaining({searchType: "basicDetails"})
+ )
+ })
+
+ it("should handle undefined parameters gracefully", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.captureOriginalSearchParameters("prescriptionId", {
+ prescriptionId: "12345",
+ issueNumber: undefined
+ })
+ })
+
+ expect(context!.getRelevantSearchParameters("prescriptionId")).toEqual({
+ prescriptionId: "12345"
+ })
+ })
+ })
+
+ describe("getBackPath", () => {
+ it("should return null when no navigation history", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ expect(context!.getBackPath()).toBeNull()
+ })
+
+ it("should handle prescription details navigation", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.pushNavigation("/prescription-list-current")
+ context!.pushNavigation("/prescription-details")
+ })
+
+ expect(context!.getBackPath()).toBe("/prescription-list-current")
+ })
+
+ it("should return original search page when available", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.setOriginalSearchPage("basicDetails")
+ })
+
+ expect(context!.getBackPath()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS
+ )
+ })
+ })
+
+ describe("goBack", () => {
+ it("should warn when no back path available", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.goBack()
+ })
+
+ expect(logger.warn).toHaveBeenCalledWith(
+ "Navigation: No back path found, staying on current page"
+ )
+ expect(mockNavigate).not.toHaveBeenCalled()
+ })
+
+ it("should navigate and update stack", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.pushNavigation("/page1")
+ context!.pushNavigation("/page2")
+ })
+
+ const backPath = context!.getBackPath()
+ expect(backPath).not.toBeNull()
+
+ await act(async () => {
+ context!.goBack()
+ })
+
+ expect(mockNavigate).toHaveBeenCalled()
+ })
+
+ it("should handle prescription list to search navigation", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.setOriginalSearchPage("basicDetails")
+ })
+
+ expect(context!.getBackPath()).toBe("/search-by-basic-details")
+
+ await act(async () => {
+ context!.goBack()
+ })
+
+ expect(mockNavigate).toHaveBeenCalledWith("/search-by-basic-details")
+ })
+ })
+
+ describe("startNewNavigationSession", () => {
+ it("should clear all navigation state", async () => {
+ let context: NavigationContextType | undefined
+ render(
+
+ (context = ctx)} />
+
+ )
+
+ await act(async () => {
+ context!.captureOriginalSearchParameters("basicDetails", {
+ firstName: "John"
+ })
+ context!.pushNavigation("/some-page")
+ context!.startNewNavigationSession()
+ })
+
+ expect(context!.getOriginalSearchParameters()).toBeNull()
+ })
+ })
+})
diff --git a/packages/cpt-ui/__tests__/NhsNumSearch.test.tsx b/packages/cpt-ui/__tests__/NhsNumSearch.test.tsx
index 65a66b34f3..402f64b44c 100644
--- a/packages/cpt-ui/__tests__/NhsNumSearch.test.tsx
+++ b/packages/cpt-ui/__tests__/NhsNumSearch.test.tsx
@@ -14,6 +14,23 @@ import NhsNumSearch from "@/components/prescriptionSearch/NhsNumSearch"
import {STRINGS} from "@/constants/ui-strings/NhsNumSearchStrings"
import {FRONTEND_PATHS} from "@/constants/environment"
import {SearchContext, SearchProviderContextType} from "@/context/SearchProvider"
+import {NavigationProvider} from "@/context/NavigationProvider"
+
+const mockNavigationContext = {
+ pushNavigation: jest.fn(),
+ goBack: jest.fn(),
+ getBackPath: jest.fn(),
+ setOriginalSearchPage: jest.fn(),
+ captureOriginalSearchParameters: jest.fn(),
+ getOriginalSearchParameters: jest.fn(),
+ getRelevantSearchParameters: jest.fn(),
+ startNewNavigationSession: jest.fn()
+}
+
+jest.mock("@/context/NavigationProvider", () => ({
+ ...jest.requireActual("@/context/NavigationProvider"),
+ useNavigationContext: () => mockNavigationContext
+}))
jest.mock("react-router-dom", () => {
const actual = jest.requireActual("react-router-dom")
@@ -32,10 +49,12 @@ const mockSetLastName = jest.fn()
const mockSetDobDay = jest.fn()
const mockSetDobMonth = jest.fn()
const mockSetDobYear = jest.fn()
-const mockSetPostcode =jest.fn()
+const mockSetPostcode = jest.fn()
const mockSetNhsNumber = jest.fn()
const mockGetAllSearchParameters = jest.fn()
const mockSetAllSearchParameters = jest.fn()
+const mockSetSearchType = jest.fn()
+
const defaultSearchState: SearchProviderContextType = {
prescriptionId: undefined,
issueNumber: undefined,
@@ -46,6 +65,7 @@ const defaultSearchState: SearchProviderContextType = {
dobYear: undefined,
postcode: undefined,
nhsNumber: undefined,
+ searchType: undefined,
clearSearchParameters: mockClearSearchParameters,
setPrescriptionId: mockSetPrescriptionId,
setIssueNumber: mockSetIssueNumber,
@@ -57,22 +77,29 @@ const defaultSearchState: SearchProviderContextType = {
setPostcode: mockSetPostcode,
setNhsNumber: mockSetNhsNumber,
getAllSearchParameters: mockGetAllSearchParameters,
- setAllSearchParameters: mockSetAllSearchParameters
+ setAllSearchParameters: mockSetAllSearchParameters,
+ setSearchType: mockSetSearchType
}
const LocationDisplay = () => {
const location = useLocation()
- return {location.pathname + location.search}
+ return (
+
+ {location.pathname + location.search}
+
+ )
}
const renderWithRouter = (ui: React.ReactElement) => {
return render(
-
-
- } />
-
+
+
+
+ } />
+
+
)
diff --git a/packages/cpt-ui/__tests__/PatientNotFoundMessage.test.tsx b/packages/cpt-ui/__tests__/PatientNotFoundMessage.test.tsx
index 5dd4aa7d4f..ad12856ba1 100644
--- a/packages/cpt-ui/__tests__/PatientNotFoundMessage.test.tsx
+++ b/packages/cpt-ui/__tests__/PatientNotFoundMessage.test.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable max-len */
+
import "@testing-library/jest-dom"
import {render, screen, fireEvent} from "@testing-library/react"
import {MemoryRouter, Routes, Route} from "react-router-dom"
@@ -6,25 +8,57 @@ import React from "react"
import PatientNotFoundMessage from "@/components/PatientNotFoundMessage"
import {STRINGS} from "@/constants/ui-strings/PatientNotFoundMessageStrings"
import {FRONTEND_PATHS} from "@/constants/environment"
+import {NavigationProvider} from "@/context/NavigationProvider"
+
+const mockGetBackPath = jest.fn()
+const mockGoBack = jest.fn()
+const mockNavigationContext = {
+ pushNavigation: jest.fn(),
+ goBack: mockGoBack,
+ getBackPath: mockGetBackPath,
+ clearNavigation: jest.fn(),
+ getCurrentEntry: jest.fn(),
+ getNavigationStack: jest.fn(),
+ canGoBack: jest.fn(),
+ setOriginalSearchPage: jest.fn(),
+ getOriginalSearchPage: jest.fn(),
+ captureOriginalSearchParameters: jest.fn(),
+ getOriginalSearchParameters: jest.fn(),
+ getRelevantSearchParameters: jest.fn(),
+ startNewNavigationSession: jest.fn()
+}
-const DummyPage = ({label}: {label: string}) => {label}
+jest.mock("@/context/NavigationProvider", () => ({
+ ...jest.requireActual("@/context/NavigationProvider"),
+ useNavigationContext: () => mockNavigationContext
+}))
+
+const DummyPage = ({label}: { label: string }) => (
+ {label}
+)
function setupRouter(
search = "?firstName=Zoe&lastName=Zero&dobDay=31&dobMonth=12&dobYear=2021&postcode=AB1%202CD"
) {
render(
-
- } />
- } />
- } />
- } />
-
+
+
+ } />
+ } />
+ } />
+ } />
+
+
)
}
describe("PatientNotFoundMessage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
it("renders the main heading and static content", () => {
setupRouter()
expect(screen.getByTestId("patient-not-found-heading")).toHaveTextContent(STRINGS.heading)
@@ -35,6 +69,10 @@ describe("PatientNotFoundMessage", () => {
it("renders the go-back link with search query, and navigates to Basic Details Search", () => {
const search = "?firstName=Zoe&lastName=Zero&dobDay=31&dobMonth=12&dobYear=2021&postcode=AB1%202CD"
+ mockGetBackPath.mockReturnValue(
+ FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS + search
+ )
+
setupRouter(search)
const backLink = screen.getByTestId("go-back-link")
expect(backLink).toHaveAttribute(
@@ -42,10 +80,12 @@ describe("PatientNotFoundMessage", () => {
FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS + search
)
fireEvent.click(backLink)
- expect(screen.getByTestId("dummy-page")).toHaveTextContent("Basic Details Search")
+ expect(mockGoBack).toHaveBeenCalled()
})
it("navigates to NHS Number Search when alternate link is clicked", () => {
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
+
setupRouter()
const nhsNumberLink = screen.getByTestId("patient-not-found-nhs-number-link")
expect(nhsNumberLink).toHaveAttribute("href", FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER)
@@ -54,6 +94,8 @@ describe("PatientNotFoundMessage", () => {
})
it("navigates to Prescription ID Search when alternate link is clicked", () => {
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
+
setupRouter()
const prescriptionIdLink = screen.getByTestId("patient-not-found-prescription-id-link")
expect(prescriptionIdLink).toHaveAttribute("href", FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID)
@@ -62,10 +104,12 @@ describe("PatientNotFoundMessage", () => {
})
it("renders correctly even with empty or no search parameter", () => {
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
+
setupRouter("") // Empty search
const backLink = screen.getByTestId("go-back-link")
expect(backLink).toHaveAttribute("href", FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
fireEvent.click(backLink)
- expect(screen.getByTestId("dummy-page")).toHaveTextContent("Basic Details Search")
+ expect(mockGoBack).toHaveBeenCalled()
})
})
diff --git a/packages/cpt-ui/__tests__/PrescriptionDetailsPage.test.tsx b/packages/cpt-ui/__tests__/PrescriptionDetailsPage.test.tsx
index 7e2b1b572a..51143d6c10 100644
--- a/packages/cpt-ui/__tests__/PrescriptionDetailsPage.test.tsx
+++ b/packages/cpt-ui/__tests__/PrescriptionDetailsPage.test.tsx
@@ -15,6 +15,7 @@ import PrescriptionDetailsPage from "@/pages/PrescriptionDetailsPage"
import http from "@/helpers/axios"
import {SearchContext, SearchProviderContextType} from "@/context/SearchProvider"
+import {NavigationProvider} from "@/context/NavigationProvider"
import {AxiosError, AxiosHeaders} from "axios"
@@ -58,6 +59,7 @@ const defaultSearchState: SearchProviderContextType = {
dobYear: undefined,
postcode: undefined,
nhsNumber: undefined,
+ searchType: undefined,
clearSearchParameters: jest.fn().mockName("clearSearchParameters"),
setPrescriptionId: jest.fn().mockName("setPrescriptionId"),
setIssueNumber: jest.fn().mockName("setIssueNumber"),
@@ -69,7 +71,8 @@ const defaultSearchState: SearchProviderContextType = {
setPostcode: jest.fn().mockName("setPostcode"),
setNhsNumber: jest.fn().mockName("setNhsNumber"),
getAllSearchParameters: jest.fn().mockName("getAllSearchParameters"),
- setAllSearchParameters: jest.fn().mockName("setAllSearchParameters")
+ setAllSearchParameters: jest.fn().mockName("setAllSearchParameters"),
+ setSearchType: jest.fn().mockName("setSearchType")
}
// Mock the spinner component.
@@ -103,11 +106,13 @@ const renderComponent = (
-
- } />
- } />
- } />
-
+
+
+ } />
+ } />
+ } />
+
+
diff --git a/packages/cpt-ui/__tests__/PrescriptionIdSearch.test.tsx b/packages/cpt-ui/__tests__/PrescriptionIdSearch.test.tsx
index 864d6bd5e5..b4f5efb905 100644
--- a/packages/cpt-ui/__tests__/PrescriptionIdSearch.test.tsx
+++ b/packages/cpt-ui/__tests__/PrescriptionIdSearch.test.tsx
@@ -14,6 +14,23 @@ import PrescriptionIdSearch from "@/components/prescriptionSearch/PrescriptionId
import {PRESCRIPTION_ID_SEARCH_STRINGS} from "@/constants/ui-strings/SearchForAPrescriptionStrings"
import {AuthContext, AuthContextType} from "@/context/AuthProvider"
import {SearchContext, SearchProviderContextType} from "@/context/SearchProvider"
+import {NavigationProvider} from "@/context/NavigationProvider"
+
+const mockNavigationContext = {
+ pushNavigation: jest.fn(),
+ goBack: jest.fn(),
+ getBackPath: jest.fn(),
+ setOriginalSearchPage: jest.fn(),
+ captureOriginalSearchParameters: jest.fn(),
+ getOriginalSearchParameters: jest.fn(),
+ getRelevantSearchParameters: jest.fn(),
+ startNewNavigationSession: jest.fn()
+}
+
+jest.mock("@/context/NavigationProvider", () => ({
+ ...jest.requireActual("@/context/NavigationProvider"),
+ useNavigationContext: () => mockNavigationContext
+}))
const mockAuthContext: AuthContextType = {
error: null,
@@ -47,10 +64,11 @@ const mockSetLastName = jest.fn()
const mockSetDobDay = jest.fn()
const mockSetDobMonth = jest.fn()
const mockSetDobYear = jest.fn()
-const mockSetPostcode =jest.fn()
+const mockSetPostcode = jest.fn()
const mockSetNhsNumber = jest.fn()
const mockGetAllSearchParameters = jest.fn()
const mockSetAllSearchParameters = jest.fn()
+const mockSetSearchType = jest.fn()
const defaultSearchState: SearchProviderContextType = {
prescriptionId: undefined,
issueNumber: undefined,
@@ -61,6 +79,7 @@ const defaultSearchState: SearchProviderContextType = {
dobYear: undefined,
postcode: undefined,
nhsNumber: undefined,
+ searchType: undefined,
clearSearchParameters: mockClearSearchParameters,
setPrescriptionId: mockSetPrescriptionId,
setIssueNumber: mockSetIssueNumber,
@@ -72,7 +91,8 @@ const defaultSearchState: SearchProviderContextType = {
setPostcode: mockSetPostcode,
setNhsNumber: mockSetNhsNumber,
getAllSearchParameters: mockGetAllSearchParameters,
- setAllSearchParameters: mockSetAllSearchParameters
+ setAllSearchParameters: mockSetAllSearchParameters,
+ setSearchType: mockSetSearchType
}
const LocationDisplay = () => {
@@ -85,10 +105,12 @@ const renderWithRouter = () =>
-
- } />
- } />
-
+
+
+ } />
+ } />
+
+
@@ -104,7 +126,10 @@ const setup = async (input: string) => {
}
describe("PrescriptionIdSearch", () => {
- beforeEach(() => jest.resetAllMocks())
+ beforeEach(() => {
+ jest.resetAllMocks()
+ jest.clearAllMocks()
+ })
it("renders label, hint, and button", () => {
renderWithRouter()
diff --git a/packages/cpt-ui/__tests__/PrescriptionListTable.test.tsx b/packages/cpt-ui/__tests__/PrescriptionListTable.test.tsx
index 191b0c71b8..baa4dd7fa1 100644
--- a/packages/cpt-ui/__tests__/PrescriptionListTable.test.tsx
+++ b/packages/cpt-ui/__tests__/PrescriptionListTable.test.tsx
@@ -10,6 +10,7 @@ import {PrescriptionsListStrings} from "@/constants/ui-strings/PrescriptionListT
import {PrescriptionSummary, TreatmentType} from "@cpt-ui-common/common-types"
import {MemoryRouter} from "react-router-dom"
import {SearchContext, SearchProviderContextType} from "@/context/SearchProvider"
+import {NavigationProvider} from "@/context/NavigationProvider"
jest.mock("@/helpers/statusMetadata", () => ({
getStatusTagColour: jest.fn().mockReturnValue("blue"),
@@ -17,6 +18,31 @@ jest.mock("@/helpers/statusMetadata", () => ({
formatDateForPrescriptions: jest.fn((date: string) => "Formatted: " + date)
}))
+// Mock the navigation context
+const mockGetRelevantSearchParameters = jest.fn()
+const mockGetOriginalSearchParameters = jest.fn()
+const mockGoBack = jest.fn()
+const mockNavigationContext = {
+ pushNavigation: jest.fn(),
+ goBack: mockGoBack,
+ getBackPath: jest.fn(),
+ clearNavigation: jest.fn(),
+ getCurrentEntry: jest.fn(),
+ getNavigationStack: jest.fn(),
+ canGoBack: jest.fn(),
+ setOriginalSearchPage: jest.fn(),
+ getOriginalSearchPage: jest.fn(),
+ captureOriginalSearchParameters: jest.fn(),
+ getOriginalSearchParameters: mockGetOriginalSearchParameters,
+ getRelevantSearchParameters: mockGetRelevantSearchParameters,
+ startNewNavigationSession: jest.fn()
+}
+
+jest.mock("@/context/NavigationProvider", () => ({
+ ...jest.requireActual("@/context/NavigationProvider"),
+ useNavigationContext: () => mockNavigationContext
+}))
+
const mockClearSearchParameters = jest.fn()
const mockSetPrescriptionId = jest.fn()
const mockSetIssueNumber = jest.fn()
@@ -25,10 +51,12 @@ const mockSetLastName = jest.fn()
const mockSetDobDay = jest.fn()
const mockSetDobMonth = jest.fn()
const mockSetDobYear = jest.fn()
-const mockSetPostcode =jest.fn()
+const mockSetPostcode = jest.fn()
const mockSetNhsNumber = jest.fn()
const mockGetAllSearchParameters = jest.fn()
const mockSetAllSearchParameters = jest.fn()
+const mockSetSearchType = jest.fn()
+
const defaultSearchState: SearchProviderContextType = {
prescriptionId: undefined,
issueNumber: undefined,
@@ -39,6 +67,7 @@ const defaultSearchState: SearchProviderContextType = {
dobYear: undefined,
postcode: undefined,
nhsNumber: undefined,
+ searchType: undefined,
clearSearchParameters: mockClearSearchParameters,
setPrescriptionId: mockSetPrescriptionId,
setIssueNumber: mockSetIssueNumber,
@@ -50,7 +79,8 @@ const defaultSearchState: SearchProviderContextType = {
setPostcode: mockSetPostcode,
setNhsNumber: mockSetNhsNumber,
getAllSearchParameters: mockGetAllSearchParameters,
- setAllSearchParameters: mockSetAllSearchParameters
+ setAllSearchParameters: mockSetAllSearchParameters,
+ setSearchType: mockSetSearchType
}
describe("PrescriptionsListTable", () => {
@@ -102,7 +132,7 @@ describe("PrescriptionsListTable", () => {
return render(
- {component}
+ {component}
)
@@ -110,6 +140,10 @@ describe("PrescriptionsListTable", () => {
beforeEach(() => {
jest.clearAllMocks()
+
+ // reset navigation context mocks
+ mockGetRelevantSearchParameters.mockReturnValue({})
+ mockGetOriginalSearchParameters.mockReturnValue({})
})
it("renders the full table container, table, and table head when prescriptions exist", async () => {
@@ -397,9 +431,10 @@ describe("PrescriptionsListTable", () => {
expect(prescriptionButton).toBeInTheDocument()
fireEvent.click(prescriptionButton)
- expect(mockClearSearchParameters).toHaveBeenCalled()
- expect(mockSetPrescriptionId).toHaveBeenCalledWith("C0C757-A83008-C2D93O")
- expect(mockSetIssueNumber).toHaveBeenCalledWith("1")
+ expect(mockSetAllSearchParameters).toHaveBeenCalledWith({
+ prescriptionId: "C0C757-A83008-C2D93O",
+ issueNumber: "1"
+ })
})
})
diff --git a/packages/cpt-ui/__tests__/PrescriptionNotFoundMessage.test.tsx b/packages/cpt-ui/__tests__/PrescriptionNotFoundMessage.test.tsx
index 3a5ea0eec6..ffc82e3c74 100644
--- a/packages/cpt-ui/__tests__/PrescriptionNotFoundMessage.test.tsx
+++ b/packages/cpt-ui/__tests__/PrescriptionNotFoundMessage.test.tsx
@@ -6,6 +6,30 @@ import PrescriptionNotFoundMessage from "@/components/PrescriptionNotFoundMessag
import {STRINGS, SEARCH_STRINGS, SEARCH_TYPES} from "@/constants/ui-strings/PrescriptionNotFoundMessageStrings"
import {FRONTEND_PATHS} from "@/constants/environment"
import {SearchContext, SearchProviderContextType} from "@/context/SearchProvider"
+import {NavigationProvider} from "@/context/NavigationProvider"
+
+const mockGetBackPath = jest.fn()
+const mockGoBack = jest.fn()
+const mockNavigationContext = {
+ pushNavigation: jest.fn(),
+ goBack: mockGoBack,
+ getBackPath: mockGetBackPath,
+ clearNavigation: jest.fn(),
+ getCurrentEntry: jest.fn(),
+ getNavigationStack: jest.fn(),
+ canGoBack: jest.fn(),
+ setOriginalSearchPage: jest.fn(),
+ getOriginalSearchPage: jest.fn(),
+ captureOriginalSearchParameters: jest.fn(),
+ getOriginalSearchParameters: jest.fn(),
+ getRelevantSearchParameters: jest.fn(),
+ startNewNavigationSession: jest.fn()
+}
+
+jest.mock("@/context/NavigationProvider", () => ({
+ ...jest.requireActual("@/context/NavigationProvider"),
+ useNavigationContext: () => mockNavigationContext
+}))
const mockClearSearchParameters = jest.fn()
const mockSetPrescriptionId = jest.fn()
@@ -15,10 +39,12 @@ const mockSetLastName = jest.fn()
const mockSetDobDay = jest.fn()
const mockSetDobMonth = jest.fn()
const mockSetDobYear = jest.fn()
-const mockSetPostcode =jest.fn()
+const mockSetPostcode = jest.fn()
const mockSetNhsNumber = jest.fn()
const mockGetAllSearchParameters = jest.fn()
const mockSetAllSearchParameters = jest.fn()
+const mockSetSearchType = jest.fn()
+
const defaultSearchState: SearchProviderContextType = {
prescriptionId: undefined,
issueNumber: undefined,
@@ -29,6 +55,7 @@ const defaultSearchState: SearchProviderContextType = {
dobYear: undefined,
postcode: undefined,
nhsNumber: undefined,
+ searchType: undefined,
clearSearchParameters: mockClearSearchParameters,
setPrescriptionId: mockSetPrescriptionId,
setIssueNumber: mockSetIssueNumber,
@@ -40,24 +67,27 @@ const defaultSearchState: SearchProviderContextType = {
setPostcode: mockSetPostcode,
setNhsNumber: mockSetNhsNumber,
getAllSearchParameters: mockGetAllSearchParameters,
- setAllSearchParameters: mockSetAllSearchParameters
+ setAllSearchParameters: mockSetAllSearchParameters,
+ setSearchType: mockSetSearchType
}
-const DummyPage = ({label}: {label: string}) => {label}
+const DummyPage = ({label}: { label: string }) => (
+ {label}
+)
interface searchParams {
- prescriptionId?: string
- issueNumber?: string
- firstName?: string
- lastName?: string
- dobDay?: string
- dobMonth?: string
- dobYear?: string
- postcode?: string
- nhsNumber?: string
+ prescriptionId?: string;
+ issueNumber?: string;
+ firstName?: string;
+ lastName?: string;
+ dobDay?: string;
+ dobMonth?: string;
+ dobYear?: string;
+ postcode?: string;
+ nhsNumber?: string;
}
const defaultSearchParams: searchParams = {
- firstName:"Zoe",
+ firstName: "Zoe",
lastName: "Zero",
dobDay: "31",
dobMonth: "12",
@@ -66,11 +96,20 @@ const defaultSearchParams: searchParams = {
}
// Helper to DRY test setup for different query params
-function setupRouter(
- searchParams: searchParams = defaultSearchParams
-) {
+function setupRouter(searchParams: searchParams = {}) {
+ let backPath = FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS
+ if (searchParams.prescriptionId) {
+ backPath = FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID
+ } else if (searchParams.nhsNumber) {
+ backPath = FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER
+ } else if (searchParams.firstName || searchParams.lastName) {
+ backPath = FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS
+ }
+ mockGetBackPath.mockReturnValue(backPath)
const searchState = {
...defaultSearchState,
+ prescriptionId: searchParams.prescriptionId,
+ issueNumber: searchParams.issueNumber,
firstName: searchParams.firstName,
lastName: searchParams.lastName,
dobDay: searchParams.dobDay,
@@ -82,27 +121,32 @@ function setupRouter(
render(
-
- } />
- } />
- } />
- } />
-
+
+
+ } />
+ } />
+ } />
+ } />
+
+
)
}
describe("PrescriptionNotFoundMessage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
it("renders the main heading and static content for basic details search", () => {
- setupRouter()
+ setupRouter(defaultSearchParams)
const headings = screen.getAllByTestId("presc-not-found-heading")
expect(headings[0]).toHaveTextContent(STRINGS.heading)
})
it("renders the main container with correct id and class", () => {
- setupRouter()
+ setupRouter(defaultSearchParams)
const mainElement = screen.getByRole("main")
expect(mainElement).toBeInTheDocument()
expect(mainElement).toHaveAttribute("id", "main-content")
@@ -110,14 +154,14 @@ describe("PrescriptionNotFoundMessage", () => {
})
it("renders the back link with correct text for basic details search", () => {
- setupRouter()
+ setupRouter(defaultSearchParams)
const link = screen.getByTestId("go-back-link")
expect(link).toHaveTextContent(STRINGS.goBackLink)
expect(link.getAttribute("href")).toContain(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
})
it("renders all body paragraphs and alternative links for basic details search", () => {
- setupRouter()
+ setupRouter(defaultSearchParams)
const querySummary = screen.getByTestId("query-summary")
// First paragraph
@@ -136,7 +180,7 @@ describe("PrescriptionNotFoundMessage", () => {
// Middle paragraph: two links
const links = within(querySummary).getAllByRole("link")
- const altLabels = links.map(link => link.textContent)
+ const altLabels = links.map((link) => link.textContent)
expect(altLabels).toEqual(
expect.arrayContaining([
"search using a prescription ID",
@@ -153,26 +197,60 @@ describe("PrescriptionNotFoundMessage", () => {
nhsNumber: "9912003071"
})
const link = screen.getByTestId("go-back-link")
- expect(link.getAttribute("href")).toContain(FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER)
+ expect(link.getAttribute("href")).toContain(
+ FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER
+ )
const querySummary = screen.getByTestId("query-summary")
+ // should show the NHS number search body text
+ expect(querySummary).toHaveTextContent(
+ "We could not find any prescriptions using the NHS number you searched for."
+ )
+
// Should offer prescription ID and basic details as alternative links
- const altLinks = within(querySummary).getAllByRole("link").map(l => l.textContent)
- const nhsNumberAltLabels = SEARCH_STRINGS[SEARCH_TYPES.NHS_NUMBER].alternatives.map(a => a.label)
+ const altLinks = within(querySummary)
+ .getAllByRole("link")
+ .map((l) => l.textContent)
+ const nhsNumberAltLabels = SEARCH_STRINGS[
+ SEARCH_TYPES.NHS_NUMBER
+ ].alternatives.map((a) => a.label)
expect(altLinks).toEqual(expect.arrayContaining(nhsNumberAltLabels))
})
+ it("renders correct navigation for NHS number search with different context", () => {
+ mockGetAllSearchParameters.mockReturnValue({
+ nhsNumber: "9912003071"
+ })
+ setupRouter({
+ nhsNumber: "9912003071"
+ })
+ const link = screen.getByTestId("go-back-link")
+ expect(link.getAttribute("href")).toContain(
+ FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER
+ )
+ })
+
it("renders correct navigation and content for Prescription ID search", () => {
setupRouter({prescriptionId: "9000000001"})
const link = screen.getByTestId("go-back-link")
- expect(link.getAttribute("href")).toContain(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
+ expect(link.getAttribute("href")).toContain(
+ FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID
+ )
const querySummary = screen.getByTestId("query-summary")
// Should offer NHS number and basic details as alternative links
const altLinks = within(querySummary).getAllByRole("link").map(l => l.textContent)
- const expectedAltLabels = SEARCH_STRINGS[SEARCH_TYPES.BASIC_DETAILS].alternatives.map(a => a.label)
+ const expectedAltLabels = SEARCH_STRINGS[SEARCH_TYPES.PRESCRIPTION_ID].alternatives.map(a => a.label)
expect(altLinks).toEqual(expect.arrayContaining(expectedAltLabels))
})
+
+ it("renders correct navigation for NHS number search", () => {
+ setupRouter({nhsNumber: "9912003071"})
+ const link = screen.getByTestId("go-back-link")
+ expect(link.getAttribute("href")).toContain(
+ FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER
+ )
+ })
})
diff --git a/packages/cpt-ui/__tests__/SearchProvider.test.tsx b/packages/cpt-ui/__tests__/SearchProvider.test.tsx
index 13df24b39b..9e23f1c690 100644
--- a/packages/cpt-ui/__tests__/SearchProvider.test.tsx
+++ b/packages/cpt-ui/__tests__/SearchProvider.test.tsx
@@ -1,5 +1,6 @@
import React from "react"
import {render, screen} from "@testing-library/react"
+import {MemoryRouter} from "react-router-dom"
import {SearchProvider, useSearchContext} from "@/context/SearchProvider"
import userEvent from "@testing-library/user-event"
@@ -27,9 +28,11 @@ const TestComponent = () => {
describe("SearchProvider", () => {
it("provides default values", () => {
render(
-
-
-
+
+
+
+
+
)
expect(screen.getByText(/Prescription ID: null/)).toBeInTheDocument()
@@ -38,9 +41,11 @@ describe("SearchProvider", () => {
it("sets values correctly", async () => {
render(
-
-
-
+
+
+
+
+
)
const user = userEvent.setup()
@@ -53,9 +58,11 @@ describe("SearchProvider", () => {
it("clears values with clearSearchParameters", async () => {
render(
-
-
-
+
+
+
+
+
)
const user = userEvent.setup()
diff --git a/packages/cpt-ui/__tests__/SearchResultsTooManyMessage.test.tsx b/packages/cpt-ui/__tests__/SearchResultsTooManyMessage.test.tsx
index b9a785725e..1203e814cc 100644
--- a/packages/cpt-ui/__tests__/SearchResultsTooManyMessage.test.tsx
+++ b/packages/cpt-ui/__tests__/SearchResultsTooManyMessage.test.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable max-len */
import "@testing-library/jest-dom"
import {render, screen, fireEvent} from "@testing-library/react"
import {
@@ -11,9 +12,34 @@ import React from "react"
import SearchResultsTooManyMessage from "@/components/SearchResultsTooManyMessage"
import {STRINGS} from "@/constants/ui-strings/SearchResultsTooManyStrings"
import {FRONTEND_PATHS} from "@/constants/environment"
+import {NavigationProvider} from "@/context/NavigationProvider"
+
+// Mock the navigation context
+const mockGetBackPath = jest.fn()
+const mockGoBack = jest.fn()
+const mockNavigationContext = {
+ pushNavigation: jest.fn(),
+ goBack: mockGoBack,
+ getBackPath: mockGetBackPath,
+ clearNavigation: jest.fn(),
+ getCurrentEntry: jest.fn(),
+ getNavigationStack: jest.fn(),
+ canGoBack: jest.fn(),
+ setOriginalSearchPage: jest.fn(),
+ getOriginalSearchPage: jest.fn(),
+ captureOriginalSearchParameters: jest.fn(),
+ getOriginalSearchParameters: jest.fn(),
+ getRelevantSearchParameters: jest.fn(),
+ startNewNavigationSession: jest.fn()
+}
+
+jest.mock("@/context/NavigationProvider", () => ({
+ ...jest.requireActual("@/context/NavigationProvider"),
+ useNavigationContext: () => mockNavigationContext
+}))
// Dummy component to receive navigation
-const DummyPage = ({label}: {label: string}) => {label}
+const DummyPage = ({label}: { label: string }) => {label}
// useLocation wrapper to pass .search to the component
function TestWrapper() {
@@ -34,17 +60,23 @@ const renderWithRouter = (
const search = makeQuery(queryParams)
return render(
-
- } />
- } />
- } />
- } />
-
+
+
+ } />
+ } />
+ } />
+ } />
+
+
)
}
describe("SearchResultsTooManyMessage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
+ })
it("renders static text content", () => {
renderWithRouter()
expect(screen.getByTestId("too-many-results-heading")).toHaveTextContent(STRINGS.heading)
@@ -79,7 +111,11 @@ describe("SearchResultsTooManyMessage", () => {
it("navigates to the basic details search when the go back link is clicked", () => {
renderWithRouter()
- fireEvent.click(screen.getByTestId("go-back-link"))
- expect(screen.getByTestId("dummy-page")).toHaveTextContent("Basic Details Search")
+ const backLink = screen.getByTestId("go-back-link")
+ expect(backLink.getAttribute("href")).toContain(
+ FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS
+ )
+ fireEvent.click(backLink)
+ expect(mockGoBack).toHaveBeenCalled()
})
})
diff --git a/packages/cpt-ui/__tests__/UnknownErrorMessage.test.tsx b/packages/cpt-ui/__tests__/UnknownErrorMessage.test.tsx
index 760ced70a8..8408704ffc 100644
--- a/packages/cpt-ui/__tests__/UnknownErrorMessage.test.tsx
+++ b/packages/cpt-ui/__tests__/UnknownErrorMessage.test.tsx
@@ -4,6 +4,30 @@ import {MemoryRouter, Routes, Route} from "react-router-dom"
import UnknownErrorMessage from "@/components/UnknownErrorMessage"
import {FRONTEND_PATHS} from "@/constants/environment"
import {SearchContext, SearchProviderContextType} from "@/context/SearchProvider"
+import {NavigationProvider} from "@/context/NavigationProvider"
+
+const mockGetBackPath = jest.fn()
+const mockGoBack = jest.fn()
+const mockNavigationContext = {
+ pushNavigation: jest.fn(),
+ goBack: mockGoBack,
+ getBackPath: mockGetBackPath,
+ clearNavigation: jest.fn(),
+ getCurrentEntry: jest.fn(),
+ getNavigationStack: jest.fn(),
+ canGoBack: jest.fn(),
+ setOriginalSearchPage: jest.fn(),
+ getOriginalSearchPage: jest.fn(),
+ captureOriginalSearchParameters: jest.fn(),
+ getOriginalSearchParameters: jest.fn(),
+ getRelevantSearchParameters: jest.fn(),
+ startNewNavigationSession: jest.fn()
+}
+
+jest.mock("@/context/NavigationProvider", () => ({
+ ...jest.requireActual("@/context/NavigationProvider"),
+ useNavigationContext: () => mockNavigationContext
+}))
const mockClearSearchParameters = jest.fn()
const mockSetPrescriptionId = jest.fn()
@@ -13,10 +37,12 @@ const mockSetLastName = jest.fn()
const mockSetDobDay = jest.fn()
const mockSetDobMonth = jest.fn()
const mockSetDobYear = jest.fn()
-const mockSetPostcode =jest.fn()
+const mockSetPostcode = jest.fn()
const mockSetNhsNumber = jest.fn()
const mockGetAllSearchParameters = jest.fn()
const mockSetAllSearchParameters = jest.fn()
+const mockSetSearchType = jest.fn()
+
const defaultSearchState: SearchProviderContextType = {
prescriptionId: undefined,
issueNumber: undefined,
@@ -27,6 +53,7 @@ const defaultSearchState: SearchProviderContextType = {
dobYear: undefined,
postcode: undefined,
nhsNumber: undefined,
+ searchType: undefined,
clearSearchParameters: mockClearSearchParameters,
setPrescriptionId: mockSetPrescriptionId,
setIssueNumber: mockSetIssueNumber,
@@ -38,7 +65,8 @@ const defaultSearchState: SearchProviderContextType = {
setPostcode: mockSetPostcode,
setNhsNumber: mockSetNhsNumber,
getAllSearchParameters: mockGetAllSearchParameters,
- setAllSearchParameters: mockSetAllSearchParameters
+ setAllSearchParameters: mockSetAllSearchParameters,
+ setSearchType: mockSetSearchType
}
// A simple error boundary for testing purposes
@@ -67,16 +95,23 @@ function ErrorThrowingComponent(): React.ReactElement {
}
describe("UnknownErrorMessage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
+ })
+
it("renders fallback UI when an unexpected error occurs", () => {
render(
-
-
-
- } />
-
-
-
+
+
+
+
+ } />
+
+
+
+
)
diff --git a/packages/cpt-ui/__tests__/useBackNavigation.test.tsx b/packages/cpt-ui/__tests__/useBackNavigation.test.tsx
new file mode 100644
index 0000000000..f03d0dbf65
--- /dev/null
+++ b/packages/cpt-ui/__tests__/useBackNavigation.test.tsx
@@ -0,0 +1,124 @@
+import {renderHook} from "@testing-library/react"
+import {useBackNavigation} from "@/hooks/useBackNavigation"
+import {useNavigationContext} from "@/context/NavigationProvider"
+import {FRONTEND_PATHS} from "@/constants/environment"
+
+jest.mock("@/context/NavigationProvider", () => ({
+ useNavigationContext: jest.fn()
+}))
+
+const mockUseNavigationContext = useNavigationContext as jest.MockedFunction<
+ typeof useNavigationContext
+>
+
+describe("useBackNavigation", () => {
+ const mockGoBack = jest.fn()
+ const mockGetBackPath = jest.fn()
+
+ beforeEach(() => {
+ mockUseNavigationContext.mockReturnValue({
+ goBack: mockGoBack,
+ getBackPath: mockGetBackPath,
+ pushNavigation: jest.fn(),
+ setOriginalSearchPage: jest.fn(),
+ captureOriginalSearchParameters: jest.fn(),
+ getOriginalSearchParameters: jest.fn(),
+ getRelevantSearchParameters: jest.fn(),
+ startNewNavigationSession: jest.fn()
+ })
+ jest.clearAllMocks()
+ })
+
+ describe("getBackLink", () => {
+ it("returns navigation context back path when available", () => {
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID)
+
+ const {result} = renderHook(() => useBackNavigation())
+ expect(result.current.getBackLink()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID
+ )
+ })
+
+ it("returns default search when navigation context returns null", () => {
+ mockGetBackPath.mockReturnValue(null)
+
+ const {result} = renderHook(() => useBackNavigation())
+ expect(result.current.getBackLink()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID
+ )
+ })
+
+ it("returns NHS number search from navigation context", () => {
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER)
+
+ const {result} = renderHook(() => useBackNavigation())
+ expect(result.current.getBackLink()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER
+ )
+ })
+
+ it("returns basic details search from navigation context", () => {
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
+
+ const {result} = renderHook(() => useBackNavigation())
+ expect(result.current.getBackLink()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS
+ )
+ })
+
+ it("returns basic details search from patient search results page", () => {
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS)
+
+ const {result} = renderHook(() => useBackNavigation())
+ expect(result.current.getBackLink()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS
+ )
+ })
+ })
+
+ describe("goBack behavior", () => {
+ it("calls navigation context goBack", () => {
+ const {result} = renderHook(() => useBackNavigation())
+
+ result.current.goBack()
+
+ expect(mockGoBack).toHaveBeenCalledTimes(1)
+ })
+
+ it("delegates navigation logic to navigation context", () => {
+ const {result} = renderHook(() => useBackNavigation())
+
+ result.current.goBack()
+ result.current.goBack()
+
+ expect(mockGoBack).toHaveBeenCalledTimes(2)
+ })
+ })
+
+ describe("hook dependencies", () => {
+ it("updates when navigation context changes", () => {
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID)
+
+ const {result, rerender} = renderHook(() => useBackNavigation())
+ expect(result.current.getBackLink()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID
+ )
+
+ mockGetBackPath.mockReturnValue(FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER)
+
+ rerender()
+ expect(result.current.getBackLink()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER
+ )
+ })
+
+ it("handles navigation context returning null", () => {
+ mockGetBackPath.mockReturnValue(null)
+
+ const {result} = renderHook(() => useBackNavigation())
+ expect(result.current.getBackLink()).toBe(
+ FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID
+ )
+ })
+ })
+})
diff --git a/packages/cpt-ui/src/App.tsx b/packages/cpt-ui/src/App.tsx
index 19c20c9c79..619c2f4538 100644
--- a/packages/cpt-ui/src/App.tsx
+++ b/packages/cpt-ui/src/App.tsx
@@ -2,6 +2,7 @@ import {Routes, Route} from "react-router-dom"
import {AuthProvider} from "@/context/AuthProvider"
import {AccessProvider} from "@/context/AccessProvider"
import {SearchProvider} from "@/context/SearchProvider"
+import {NavigationProvider} from "@/context/NavigationProvider"
import {PatientDetailsProvider} from "./context/PatientDetailsProvider"
import {PrescriptionInformationProvider} from "./context/PrescriptionInformationProvider"
import Layout from "@/Layout"
@@ -33,32 +34,34 @@ export default function App() {
-
- }>
- {/* Public cookie routes */}
- } />
- } />
+
+
+ }>
+ {/* Public cookie routes */}
+ } />
+ } />
- {/* Your existing routes */}
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
+ {/* Your existing routes */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
diff --git a/packages/cpt-ui/src/components/EpsBackLink.tsx b/packages/cpt-ui/src/components/EpsBackLink.tsx
new file mode 100644
index 0000000000..177af9f95c
--- /dev/null
+++ b/packages/cpt-ui/src/components/EpsBackLink.tsx
@@ -0,0 +1,25 @@
+import React from "react"
+import {BackLink} from "nhsuk-react-components"
+import {useBackNavigation} from "@/hooks/useBackNavigation"
+
+export default function EpsBackLink({children, ...props}: React.ComponentProps) {
+ const {goBack, getBackLink} = useBackNavigation()
+
+ const handleGoBack = (e: React.MouseEvent | React.KeyboardEvent) => {
+ e.preventDefault()
+ goBack()
+ }
+
+ return (
+ e.key === "Enter" && handleGoBack(e)}
+ {...props}
+ >
+ {children}
+
+ )
+}
diff --git a/packages/cpt-ui/src/components/PatientNotFoundMessage.tsx b/packages/cpt-ui/src/components/PatientNotFoundMessage.tsx
index 9bef2a1494..02fa62048f 100644
--- a/packages/cpt-ui/src/components/PatientNotFoundMessage.tsx
+++ b/packages/cpt-ui/src/components/PatientNotFoundMessage.tsx
@@ -1,31 +1,20 @@
import React from "react"
-import {
- Container,
- Row,
- Col,
- BackLink
-} from "nhsuk-react-components"
+import {Container, Row, Col} from "nhsuk-react-components"
import {Link} from "react-router-dom"
import {FRONTEND_PATHS} from "@/constants/environment"
import {STRINGS} from "@/constants/ui-strings/PatientNotFoundMessageStrings"
+import EpsBackLink from "@/components/EpsBackLink"
-type PatientNotFoundMessageProps = {
- readonly search?: string
-}
-
-export default function PatientNotFoundMessage({search = ""}: PatientNotFoundMessageProps) {
+export default function PatientNotFoundMessage() {
return (
-
+
{STRINGS.goBackLink}
-
+
{
switch (prescriptionType) {
case "0001":
@@ -65,7 +65,9 @@ const PrescriptionsListTable = ({
case "0002":
return PRESCRIPTION_LIST_TABLE_TEXT.typeDisplayText.repeat
case "0003":
- return PRESCRIPTION_LIST_TABLE_TEXT.typeDisplayText.erd.replace("Y", repeatIssue?.toString()).replace("X", repeatMax!.toString())
+ return PRESCRIPTION_LIST_TABLE_TEXT.typeDisplayText.erd
+ .replace("Y", repeatIssue?.toString())
+ .replace("X", repeatMax!.toString())
default:
return PRESCRIPTION_LIST_TABLE_TEXT.typeDisplayText.unknown
}
@@ -91,13 +93,23 @@ const PrescriptionsListTable = ({
aria-label={PRESCRIPTION_LIST_TABLE_TEXT.sortLabel}
>
▲
▼
@@ -128,10 +140,36 @@ const PrescriptionsListTable = ({
return `${FRONTEND_PATHS.PRESCRIPTION_DETAILS_PAGE}`
}
- const setSearchPrescriptionState = (prescriptionId: string, issueNumber: string | undefined) => {
- searchContext.clearSearchParameters()
- searchContext.setPrescriptionId(prescriptionId)
- searchContext.setIssueNumber(issueNumber)
+ const setSearchPrescriptionState = (
+ prescriptionId: string,
+ issueNumber: string | undefined
+ ) => {
+ const originalSearchParams =
+ navigationContext.getOriginalSearchParameters()
+
+ let relevantParams = {}
+ if (originalSearchParams) {
+ if (originalSearchParams.prescriptionId) {
+ relevantParams =
+ navigationContext.getRelevantSearchParameters("prescriptionId")
+ } else if (originalSearchParams.nhsNumber) {
+ relevantParams =
+ navigationContext.getRelevantSearchParameters("nhsNumber")
+ } else if (
+ originalSearchParams.firstName ||
+ originalSearchParams.lastName
+ ) {
+ relevantParams =
+ navigationContext.getRelevantSearchParameters("basicDetails")
+ }
+ }
+
+ // only preserve relevant search parameters and add prescription-specific ones
+ searchContext.setAllSearchParameters({
+ ...relevantParams,
+ prescriptionId,
+ issueNumber
+ })
}
const getSortedItems = () => {
@@ -196,7 +234,7 @@ const PrescriptionsListTable = ({
const intro =
PRESCRIPTION_LIST_TABLE_TEXT.caption[
- testid as keyof typeof PRESCRIPTION_LIST_TABLE_TEXT.caption
+ testid as keyof typeof PRESCRIPTION_LIST_TABLE_TEXT.caption
]
const sharedText = PRESCRIPTION_LIST_TABLE_TEXT.caption.sharedText
diff --git a/packages/cpt-ui/src/components/prescriptionSearch/BasicDetailsSearch.tsx b/packages/cpt-ui/src/components/prescriptionSearch/BasicDetailsSearch.tsx
index 29cfa6b5aa..1626b54839 100644
--- a/packages/cpt-ui/src/components/prescriptionSearch/BasicDetailsSearch.tsx
+++ b/packages/cpt-ui/src/components/prescriptionSearch/BasicDetailsSearch.tsx
@@ -20,6 +20,7 @@ import {errorFocusMap, ErrorKey, resolveDobInvalidFields} from "@/helpers/basicD
import {STRINGS} from "@/constants/ui-strings/BasicDetailsSearchStrings"
import {FRONTEND_PATHS} from "@/constants/environment"
import {useSearchContext} from "@/context/SearchProvider"
+import {useNavigationContext} from "@/context/NavigationProvider"
export default function BasicDetailsSearch() {
const navigate = useNavigate()
@@ -36,6 +37,7 @@ export default function BasicDetailsSearch() {
const inlineErrors = getInlineErrors(errors)
const searchContext = useSearchContext()
+ const navigationContext = useNavigationContext()
// Inline error lookup: used to find the error message string for specific field(s)
// Returns the first match found in the array of inline error tuples
@@ -49,6 +51,19 @@ export default function BasicDetailsSearch() {
}
}, [errors])
+ // restore original search parameters when available
+ useEffect(() => {
+ const originalParams = navigationContext.getOriginalSearchParameters()
+ if (originalParams) {
+ setFirstName(originalParams.firstName || "")
+ setLastName(originalParams.lastName || "")
+ setDobDay(originalParams.dobDay || "")
+ setDobMonth(originalParams.dobMonth || "")
+ setDobYear(originalParams.dobYear || "")
+ setPostcode(originalParams.postcode || "")
+ }
+ }, [navigationContext])
+
useEffect(() => {
// Allows keyboard/screen-reader users to jump to field when clicking summary links
// Needed for tests: jsdom doesn't auto-focus elements via href="#field-id" links.
@@ -115,10 +130,14 @@ export default function BasicDetailsSearch() {
"dobFutureDate"
])
- const hasDobRelatedError = newErrors.some(error => dobErrorKeys.has(error))
+ const hasDobRelatedError = newErrors.some((error) =>
+ dobErrorKeys.has(error)
+ )
if (hasDobRelatedError) {
- setDobErrorFields(resolveDobInvalidFields({dobDay, dobMonth, dobYear}))
+ setDobErrorFields(
+ resolveDobInvalidFields({dobDay, dobMonth, dobYear})
+ )
} else {
setDobErrorFields([])
}
@@ -126,6 +145,22 @@ export default function BasicDetailsSearch() {
return
}
+ //clear any previous search navigation context
+ navigationContext.startNewNavigationSession()
+
+ // capture original search parameters before clearing
+ const originalParams = {
+ firstName,
+ lastName,
+ dobDay,
+ dobMonth,
+ dobYear,
+ postcode
+ }
+ navigationContext.captureOriginalSearchParameters(
+ "basicDetails",
+ originalParams
+ )
//API requires a 2 digit format i.e. 06 instead of 6 for June
const formattedDobDay = dobDay.padStart(2, "0")
const formattedDobMonth = dobMonth.padStart(2, "0")
@@ -137,6 +172,7 @@ export default function BasicDetailsSearch() {
searchContext.setDobMonth(formattedDobMonth)
searchContext.setDobYear(dobYear)
searchContext.setPostcode(postcode)
+ searchContext.setSearchType("basicDetails")
navigate(FRONTEND_PATHS.PATIENT_SEARCH_RESULTS)
}
diff --git a/packages/cpt-ui/src/components/prescriptionSearch/NhsNumSearch.tsx b/packages/cpt-ui/src/components/prescriptionSearch/NhsNumSearch.tsx
index e16878fdd3..065a69ba93 100644
--- a/packages/cpt-ui/src/components/prescriptionSearch/NhsNumSearch.tsx
+++ b/packages/cpt-ui/src/components/prescriptionSearch/NhsNumSearch.tsx
@@ -22,19 +22,38 @@ import {
import {STRINGS} from "@/constants/ui-strings/NhsNumSearchStrings"
import {FRONTEND_PATHS} from "@/constants/environment"
import {useSearchContext} from "@/context/SearchProvider"
+import {useNavigationContext} from "@/context/NavigationProvider"
import {validateNhsNumber, normalizeNhsNumber, NhsNumberValidationError} from "@/helpers/validateNhsNumber"
export default function NhsNumSearch() {
- const [nhsNumber, setNhsNumber] = useState("")
- const [errorKey, setErrorKey] = useState(null)
- const errorRef = useRef(null)
const navigate = useNavigate()
const searchContext = useSearchContext()
+ const navigationContext = useNavigationContext()
+ const [nhsNumber, setNhsNumber] = useState(
+ searchContext.nhsNumber || ""
+ )
+ const [errorKey, setErrorKey] = useState(
+ null
+ )
+ const errorRef = useRef(null)
const errorMessages = STRINGS.errors
const displayedError = useMemo(() => errorKey ? errorMessages[errorKey] : "", [errorKey])
+ useEffect(() => {
+ if (searchContext.nhsNumber && searchContext.searchType === "nhs") {
+ setNhsNumber(searchContext.nhsNumber)
+ }
+ }, [searchContext.nhsNumber, searchContext.searchType])
+
+ useEffect(() => {
+ const originalParams = navigationContext.getOriginalSearchParameters()
+ if (originalParams && originalParams.nhsNumber) {
+ setNhsNumber(originalParams.nhsNumber || "")
+ }
+ }, [navigationContext])
+
useEffect(() => {
if (errorKey && errorRef.current) {
errorRef.current.focus()
@@ -55,8 +74,21 @@ export default function NhsNumSearch() {
}
setErrorKey(null)
const normalized = normalizeNhsNumber(nhsNumber)
+
+ // clear any previous search context
+ navigationContext.startNewNavigationSession()
+
+ const originalParams = {
+ nhsNumber: normalized || ""
+ }
+ navigationContext.captureOriginalSearchParameters(
+ "nhsNumber",
+ originalParams
+ )
+
searchContext.clearSearchParameters()
- searchContext.setNhsNumber(normalized)
+ searchContext.setNhsNumber(normalized || "")
+ searchContext.setSearchType("nhs")
navigate(`${FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT}`)
}
diff --git a/packages/cpt-ui/src/components/prescriptionSearch/PrescriptionIdSearch.tsx b/packages/cpt-ui/src/components/prescriptionSearch/PrescriptionIdSearch.tsx
index 3e09192d47..a6e2923065 100644
--- a/packages/cpt-ui/src/components/prescriptionSearch/PrescriptionIdSearch.tsx
+++ b/packages/cpt-ui/src/components/prescriptionSearch/PrescriptionIdSearch.tsx
@@ -29,20 +29,31 @@ import {
PrescriptionValidationError
} from "@/helpers/validatePrescriptionDetailsSearch"
import {useSearchContext} from "@/context/SearchProvider"
+import {useNavigationContext} from "@/context/NavigationProvider"
export default function PrescriptionIdSearch() {
const navigate = useNavigate()
const errorRef = useRef(null)
const searchContext = useSearchContext()
+ const navigationContext = useNavigationContext()
- const [prescriptionId, setPrescriptionId] = useState("")
+ const [prescriptionId, setPrescriptionId] = useState(searchContext.prescriptionId || "")
const [errorKey, setErrorKey] = useState(null)
const errorMessages = PRESCRIPTION_ID_SEARCH_STRINGS.errors
+ useEffect(() => {
+ const originalParams = navigationContext.getOriginalSearchParameters()
+ if (originalParams && originalParams.prescriptionId) {
+ setPrescriptionId(originalParams.prescriptionId || "")
+ }
+ }, [navigationContext])
+
// Maps a validation error key to the corresponding user-facing message.
// Treats "checksum" as "noMatch" to simplify the error display logic.
- const getDisplayedErrorMessage = (key: PrescriptionValidationError | null): string => {
+ const getDisplayedErrorMessage = (
+ key: PrescriptionValidationError | null
+ ): string => {
if (!key) return ""
if (key === "noMatch") return errorMessages.noMatch
return errorMessages[key] || errorMessages.noMatch
@@ -51,6 +62,15 @@ export default function PrescriptionIdSearch() {
// Memoised error message for display
const displayedError = useMemo(() => getDisplayedErrorMessage(errorKey), [errorKey])
+ useEffect(() => {
+ if (
+ searchContext.prescriptionId &&
+ searchContext.searchType === "prescriptionId"
+ ) {
+ setPrescriptionId(searchContext.prescriptionId)
+ }
+ }, [searchContext.prescriptionId, searchContext.searchType])
+
// When error is set, focus error summary
useEffect(() => {
if (errorKey && errorRef.current) errorRef.current.focus()
@@ -74,8 +94,21 @@ export default function PrescriptionIdSearch() {
setErrorKey(null) // Clear error on valid submit
const formatted = normalizePrescriptionId(prescriptionId)
+
+ //clear previous search context
+ navigationContext.startNewNavigationSession()
+
+ const originalParams = {
+ prescriptionId: formatted
+ }
+ navigationContext.captureOriginalSearchParameters(
+ "prescriptionId",
+ originalParams
+ )
+
searchContext.clearSearchParameters()
searchContext.setPrescriptionId(formatted)
+ searchContext.setSearchType("prescriptionId")
navigate(`${FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT}`)
}
diff --git a/packages/cpt-ui/src/constants/ui-strings/BasicDetailsSearchResultsPageStrings.ts b/packages/cpt-ui/src/constants/ui-strings/BasicDetailsSearchResultsPageStrings.ts
index 86bd8876cc..c96928a4c0 100644
--- a/packages/cpt-ui/src/constants/ui-strings/BasicDetailsSearchResultsPageStrings.ts
+++ b/packages/cpt-ui/src/constants/ui-strings/BasicDetailsSearchResultsPageStrings.ts
@@ -1,6 +1,6 @@
export const SearchResultsPageStrings = {
- GO_BACK: "Go back",
- TITLE: "Search Results",
+ GO_BACK: "Back",
+ TITLE: "Search results",
RESULTS_COUNT: "We found {count} results.",
LOADING: "Loading search results",
TABLE: {
diff --git a/packages/cpt-ui/src/constants/ui-strings/HeaderStrings.ts b/packages/cpt-ui/src/constants/ui-strings/HeaderStrings.ts
index 1aa54b3d75..9eb7c250f3 100644
--- a/packages/cpt-ui/src/constants/ui-strings/HeaderStrings.ts
+++ b/packages/cpt-ui/src/constants/ui-strings/HeaderStrings.ts
@@ -2,7 +2,7 @@ import {FRONTEND_PATHS} from "@/constants/environment"
// HEADER strings
export const HEADER_SERVICE = "Prescription Tracker (pilot)"
-export const HEADER_CHANGE_ROLE_BUTTON = "Change Role"
+export const HEADER_CHANGE_ROLE_BUTTON = "Change role"
export const HEADER_CHANGE_ROLE_TARGET = FRONTEND_PATHS.CHANGE_YOUR_ROLE
export const HEADER_SELECT_YOUR_ROLE_BUTTON = "Select Your Role"
export const HEADER_SELECT_YOUR_ROLE_TARGET = FRONTEND_PATHS.SELECT_YOUR_ROLE
diff --git a/packages/cpt-ui/src/constants/ui-strings/PatientNotFoundMessageStrings.ts b/packages/cpt-ui/src/constants/ui-strings/PatientNotFoundMessageStrings.ts
index 4c6ef5cd3d..d6a78264cf 100644
--- a/packages/cpt-ui/src/constants/ui-strings/PatientNotFoundMessageStrings.ts
+++ b/packages/cpt-ui/src/constants/ui-strings/PatientNotFoundMessageStrings.ts
@@ -7,5 +7,5 @@ export const STRINGS = {
orText: " or ",
nhsNumberLinkText: "search using an NHS number",
endPunctuation: ".",
- goBackLink: "Go back"
+ goBackLink: "Back"
}
diff --git a/packages/cpt-ui/src/constants/ui-strings/PrescriptionDetailsPageStrings.ts b/packages/cpt-ui/src/constants/ui-strings/PrescriptionDetailsPageStrings.ts
index bbd7f2b51f..402d53e867 100644
--- a/packages/cpt-ui/src/constants/ui-strings/PrescriptionDetailsPageStrings.ts
+++ b/packages/cpt-ui/src/constants/ui-strings/PrescriptionDetailsPageStrings.ts
@@ -1,5 +1,5 @@
export const STRINGS = {
HEADER: "Prescription details",
LOADING_FULL_PRESCRIPTION: "Loading full prescription",
- GO_BACK: "Go back"
+ GO_BACK: "Back"
}
diff --git a/packages/cpt-ui/src/constants/ui-strings/PrescriptionListPageStrings.ts b/packages/cpt-ui/src/constants/ui-strings/PrescriptionListPageStrings.ts
index 5723084690..1f0a302820 100644
--- a/packages/cpt-ui/src/constants/ui-strings/PrescriptionListPageStrings.ts
+++ b/packages/cpt-ui/src/constants/ui-strings/PrescriptionListPageStrings.ts
@@ -4,7 +4,7 @@ export const PRESCRIPTION_LIST_PAGE_STRINGS = {
PAGE_TITLE: "Prescriptions list",
HEADING: "Prescriptions list",
LOADING_MESSAGE: "Loading search results",
- GO_BACK_LINK_TEXT: "Go back",
+ GO_BACK_LINK_TEXT: "Back",
RESULTS_PREFIX: "We found ",
RESULTS_SUFFIX: " results",
DEFAULT_BACK_LINK_TARGET: FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID,
diff --git a/packages/cpt-ui/src/constants/ui-strings/PrescriptionNotFoundMessageStrings.ts b/packages/cpt-ui/src/constants/ui-strings/PrescriptionNotFoundMessageStrings.ts
index d583dba723..ca68df540e 100644
--- a/packages/cpt-ui/src/constants/ui-strings/PrescriptionNotFoundMessageStrings.ts
+++ b/packages/cpt-ui/src/constants/ui-strings/PrescriptionNotFoundMessageStrings.ts
@@ -64,7 +64,7 @@ export const SEARCH_STRINGS = {
export const STRINGS = {
heading: "No prescriptions found",
- goBackLink: "Go back"
+ goBackLink: "Back"
} as const
export const SEARCH_TYPES = {
diff --git a/packages/cpt-ui/src/constants/ui-strings/SearchResultsTooManyStrings.ts b/packages/cpt-ui/src/constants/ui-strings/SearchResultsTooManyStrings.ts
index 5dd4bc222f..b016260b55 100644
--- a/packages/cpt-ui/src/constants/ui-strings/SearchResultsTooManyStrings.ts
+++ b/packages/cpt-ui/src/constants/ui-strings/SearchResultsTooManyStrings.ts
@@ -9,5 +9,5 @@ export const STRINGS = {
orText: " or ",
prescriptionIdLinkText: "search using a prescription ID",
endPunctuation: ".",
- goBackLink: "Go back"
+ goBackLink: "Back"
}
diff --git a/packages/cpt-ui/src/constants/ui-strings/UnknownErrorMessageStrings.ts b/packages/cpt-ui/src/constants/ui-strings/UnknownErrorMessageStrings.ts
index 2b7c7bd07b..94d00095fe 100644
--- a/packages/cpt-ui/src/constants/ui-strings/UnknownErrorMessageStrings.ts
+++ b/packages/cpt-ui/src/constants/ui-strings/UnknownErrorMessageStrings.ts
@@ -1,5 +1,5 @@
export const STRINGS = {
heading: "Sorry, there is a problem with this service",
intro: "Try again later.",
- goBackLink: "Go back"
+ goBackLink: "Back"
}
diff --git a/packages/cpt-ui/src/context/NavigationProvider.tsx b/packages/cpt-ui/src/context/NavigationProvider.tsx
new file mode 100644
index 0000000000..a921b14f79
--- /dev/null
+++ b/packages/cpt-ui/src/context/NavigationProvider.tsx
@@ -0,0 +1,344 @@
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ useEffect
+} from "react"
+import {useNavigate, useLocation} from "react-router-dom"
+import {FRONTEND_PATHS} from "@/constants/environment"
+import {logger} from "@/helpers/logger"
+
+export type NavigationEntry = {
+ path: string;
+};
+
+export interface NavigationContextType {
+ pushNavigation: (path: string) => void;
+ goBack: () => void;
+ getBackPath: () => string | null;
+
+ setOriginalSearchPage: (
+ searchType: "prescriptionId" | "nhsNumber" | "basicDetails",
+ ) => void
+
+ captureOriginalSearchParameters: (
+ searchType: "prescriptionId" | "nhsNumber" | "basicDetails",
+ searchParams: Record,
+ ) => void;
+ getOriginalSearchParameters: () => Record | null;
+ getRelevantSearchParameters: (
+ searchType: "prescriptionId" | "nhsNumber" | "basicDetails",
+ ) => Record;
+
+ startNewNavigationSession: () => void;
+}
+
+const NavigationContext = createContext(null)
+
+export const useNavigationContext = () => {
+ const context = useContext(NavigationContext)
+ if (!context) {
+ throw new Error(
+ "useNavigationContext must be used within NavigationProvider"
+ )
+ }
+ return context
+}
+
+interface NavigationProviderProps {
+ children: React.ReactNode;
+}
+
+export const NavigationProvider: React.FC = ({
+ children
+}) => {
+ const navigate = useNavigate()
+ const location = useLocation()
+
+ const [navigationStack, setNavigationStack] = useState>([])
+ const [originalSearchPage, setOriginalSearchPageState] = useState<{ path: string} | null>(null)
+ const [originalSearchParameters, setOriginalSearchParameters] = useState<{
+ searchType: "prescriptionId" | "nhsNumber" | "basicDetails";
+ params: Record
+ } | null>(null)
+
+ // prescription list pages that should be treated as one logical unit
+ const prescriptionListPages = [
+ FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT,
+ FRONTEND_PATHS.PRESCRIPTION_LIST_FUTURE,
+ FRONTEND_PATHS.PRESCRIPTION_LIST_PAST
+ ]
+
+ const searchPages = [
+ FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID,
+ FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER,
+ FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS
+ ]
+
+ const pushNavigation = useCallback(
+ (path: string) => {
+ const newEntry: NavigationEntry = {
+ path
+ }
+
+ setNavigationStack((prev) => {
+ if (prev.length > 0 && prev[prev.length - 1].path === path) {
+ return prev
+ }
+
+ // special handling for prescription details navigation
+ if (path === FRONTEND_PATHS.PRESCRIPTION_DETAILS_PAGE) {
+ return [...prev, newEntry]
+ }
+
+ // if navigating to a prescription list page and we already have one in stack,
+ // replace it instead of adding a new entry (treating tabs as one logical page)
+ if (prescriptionListPages.includes(path)) {
+ const lastEntry = prev[prev.length - 1]
+ if (lastEntry && prescriptionListPages.includes(lastEntry.path)) {
+ return [...prev.slice(0, -1), newEntry]
+ }
+ }
+
+ logger.info("Navigation: Pushing entry", {
+ path,
+ stackLength: prev.length + 1
+ })
+ return [...prev, newEntry]
+ })
+ },
+ [prescriptionListPages]
+ )
+
+ const setOriginalSearchPage = useCallback(
+ (searchType: "prescriptionId" | "nhsNumber" | "basicDetails") => {
+ const searchPaths = {
+ prescriptionId: FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID,
+ nhsNumber: FRONTEND_PATHS.SEARCH_BY_NHS_NUMBER,
+ basicDetails: FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS
+ }
+
+ const searchPage = {
+ path: searchPaths[searchType]
+ }
+
+ logger.info("Navigation: Setting original search page", searchPage)
+ setOriginalSearchPageState(searchPage)
+ },
+ []
+ )
+
+ const captureOriginalSearchParameters = useCallback(
+ (
+ searchType: "prescriptionId" | "nhsNumber" | "basicDetails",
+ searchParams: Record
+ ) => {
+ logger.info("Navigation: Capturing original search parameters", {
+ searchType,
+ params: searchParams
+ })
+ setOriginalSearchParameters({
+ searchType,
+ params: {...searchParams}
+ })
+
+ // clear navigation stack to start fresh navigation session and set the original search page
+ setNavigationStack([])
+ setOriginalSearchPage(searchType)
+ },
+ [setOriginalSearchPage]
+ )
+
+ const getOriginalSearchParameters = useCallback(() => {
+ return originalSearchParameters?.params || null
+ }, [originalSearchParameters])
+
+ const getRelevantSearchParameters = useCallback(
+ (searchType: "prescriptionId" | "nhsNumber" | "basicDetails") => {
+ if (
+ !originalSearchParameters ||
+ originalSearchParameters.searchType !== searchType
+ ) {
+ return {}
+ }
+
+ // return only parameters relevant to the specified search type
+ const relevantKeys = {
+ prescriptionId: ["prescriptionId", "issueNumber"],
+ nhsNumber: ["nhsNumber"],
+ basicDetails: [
+ "firstName",
+ "lastName",
+ "dobDay",
+ "dobMonth",
+ "dobYear",
+ "postcode"
+ ]
+ }
+
+ const keys = relevantKeys[searchType] || []
+ const relevantParams: Record = {}
+
+ keys.forEach((key) => {
+ if (originalSearchParameters.params[key] !== undefined) {
+ relevantParams[key] = originalSearchParameters.params[key]
+ }
+ })
+
+ logger.info("Navigation: Getting relevant search parameters", {
+ searchType,
+ originalParams: originalSearchParameters.params,
+ relevantParams
+ })
+
+ return relevantParams
+ },
+ [originalSearchParameters]
+ )
+
+ const getBackPath = useCallback((): string | null => {
+ const currentPath = location.pathname
+
+ // if we're on prescription details page, find the last prescription list page in stack
+ if (currentPath === FRONTEND_PATHS.PRESCRIPTION_DETAILS_PAGE) {
+ for (let i = navigationStack.length - 1; i >= 0; i--) {
+ const entry = navigationStack[i]
+ if (prescriptionListPages.includes(entry.path)) {
+ return entry.path
+ }
+ }
+ return FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT
+ }
+
+ if (originalSearchPage && !prescriptionListPages.includes(currentPath)) {
+ return originalSearchPage.path
+ }
+
+ if (prescriptionListPages.includes(currentPath)) {
+ if (originalSearchPage) {
+ return originalSearchPage.path
+ }
+ // fallback to navigation stack
+ if (navigationStack.length >= 2) {
+ // find the last non-prescription-list entry
+ for (let i = navigationStack.length - 2; i >= 0; i--) {
+ if (!prescriptionListPages.includes(navigationStack[i].path)) {
+ return navigationStack[i].path
+ }
+ }
+ }
+ }
+
+ if (navigationStack.length >= 2) {
+ return navigationStack[navigationStack.length - 2].path
+ }
+
+ return null
+ }, [
+ location.pathname,
+ navigationStack,
+ originalSearchPage,
+ prescriptionListPages
+ ])
+
+ const goBack = useCallback(() => {
+ const backPath = getBackPath()
+ const currentPath = location.pathname
+
+ logger.info("Navigation: Going back", {
+ from: currentPath,
+ to: backPath,
+ stackLength: navigationStack.length,
+ originalSearchPage
+ })
+
+ if (!backPath) {
+ logger.warn("Navigation: No back path found, staying on current page")
+ return
+ }
+
+ setNavigationStack((prev) => {
+ // if going back to original search page from prescription list, clear the stack
+ if (
+ prescriptionListPages.includes(currentPath) &&
+ originalSearchPage?.path === backPath
+ ) {
+ return [{path: backPath}]
+ }
+
+ // if going back from prescription details to list, remove details entry
+ if (
+ currentPath === FRONTEND_PATHS.PRESCRIPTION_DETAILS_PAGE &&
+ prescriptionListPages.includes(backPath)
+ ) {
+ return prev.filter(
+ (entry) => entry.path !== FRONTEND_PATHS.PRESCRIPTION_DETAILS_PAGE
+ )
+ }
+
+ return prev.slice(0, -1)
+ })
+
+ navigate(backPath)
+ }, [
+ getBackPath,
+ location.pathname,
+ navigate,
+ navigationStack,
+ originalSearchPage,
+ prescriptionListPages
+ ])
+
+ const startNewNavigationSession = useCallback(() => {
+ setNavigationStack([])
+ setOriginalSearchPageState(null)
+ setOriginalSearchParameters(null)
+ }, [])
+
+ useEffect(() => {
+ const currentPath = location.pathname
+
+ const currentEntry =
+ navigationStack.length > 0
+ ? navigationStack[navigationStack.length - 1]
+ : null
+ if (currentEntry?.path === currentPath) {
+ return
+ }
+
+ if (searchPages.includes(currentPath) && !originalSearchPage) {
+ const searchType = currentPath.includes("prescription-id")
+ ? "prescriptionId"
+ : currentPath.includes("nhs-number")
+ ? "nhsNumber"
+ : "basicDetails"
+ setOriginalSearchPage(searchType)
+ }
+
+ pushNavigation(currentPath)
+ }, [
+ location.pathname,
+ navigationStack,
+ originalSearchPage,
+ pushNavigation,
+ searchPages
+ ])
+
+ const contextValue: NavigationContextType = {
+ pushNavigation,
+ goBack,
+ getBackPath,
+ setOriginalSearchPage,
+ captureOriginalSearchParameters,
+ getOriginalSearchParameters,
+ getRelevantSearchParameters,
+ startNewNavigationSession
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/packages/cpt-ui/src/context/SearchProvider.tsx b/packages/cpt-ui/src/context/SearchProvider.tsx
index f942e3cf80..6e66e4f52b 100644
--- a/packages/cpt-ui/src/context/SearchProvider.tsx
+++ b/packages/cpt-ui/src/context/SearchProvider.tsx
@@ -10,6 +10,7 @@ export interface SearchParameters {
dobYear?: string
postcode?: string
nhsNumber?: string
+ searchType?: "nhs" | "prescriptionId" | "basicDetails"
}
export interface SearchProviderContextType {
@@ -22,6 +23,7 @@ export interface SearchProviderContextType {
dobYear?: string
postcode?: string
nhsNumber?: string
+ searchType?: "nhs" | "prescriptionId" | "basicDetails"
clearSearchParameters: () => void
setPrescriptionId: (prescriptionId: string | undefined) => void
setIssueNumber: (issueNumber: string | undefined) => void
@@ -32,6 +34,9 @@ export interface SearchProviderContextType {
setDobYear: (dobYear: string | undefined) => void
setPostcode: (postCode: string | undefined) => void
setNhsNumber: (nhsNumber: string | undefined) => void
+ setSearchType: (
+ searchType: "nhs" | "prescriptionId" | "basicDetails" | undefined,
+ ) => void
getAllSearchParameters: () => SearchParameters
setAllSearchParameters: (searchParameters: SearchParameters) => void
}
@@ -48,6 +53,9 @@ export const SearchProvider = ({children}: { children: React.ReactNode }) => {
const [dobYear, setDobYear] = useState(undefined)
const [postcode, setPostcode] = useState(undefined)
const [nhsNumber, setNhsNumber] = useState(undefined)
+ const [searchType, setSearchType] = useState<
+ "nhs" | "prescriptionId" | "basicDetails" | undefined
+ >(undefined)
const clearSearchParameters = () => {
setPrescriptionId(undefined)
@@ -59,6 +67,7 @@ export const SearchProvider = ({children}: { children: React.ReactNode }) => {
setDobYear(undefined)
setPostcode(undefined)
setNhsNumber(undefined)
+ setSearchType(undefined)
}
const setAllSearchParameters = (searchParameters: SearchParameters) => {
@@ -71,6 +80,7 @@ export const SearchProvider = ({children}: { children: React.ReactNode }) => {
setDobYear(searchParameters.dobYear)
setPostcode(searchParameters.postcode)
setNhsNumber(searchParameters.nhsNumber)
+ setSearchType(searchParameters.searchType)
}
const getAllSearchParameters = () => {
@@ -83,33 +93,39 @@ export const SearchProvider = ({children}: { children: React.ReactNode }) => {
dobMonth,
dobYear,
postcode,
- nhsNumber
+ nhsNumber,
+ searchType
}
}
+
return (
-
+
{children}
)
diff --git a/packages/cpt-ui/src/hooks/useBackNavigation.tsx b/packages/cpt-ui/src/hooks/useBackNavigation.tsx
new file mode 100644
index 0000000000..8ae48d9f9b
--- /dev/null
+++ b/packages/cpt-ui/src/hooks/useBackNavigation.tsx
@@ -0,0 +1,21 @@
+import {useCallback} from "react"
+import {useNavigationContext} from "@/context/NavigationProvider"
+import {FRONTEND_PATHS} from "@/constants/environment"
+
+export const useBackNavigation = () => {
+ const navigationContext = useNavigationContext()
+
+ const getBackLink = useCallback(() => {
+ const backPath = navigationContext.getBackPath()
+ return backPath || FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID
+ }, [navigationContext])
+
+ const goBack = useCallback(() => {
+ navigationContext.goBack()
+ }, [navigationContext])
+
+ return {
+ getBackLink,
+ goBack
+ }
+}
diff --git a/packages/cpt-ui/src/pages/BasicDetailsSearchResultsPage.tsx b/packages/cpt-ui/src/pages/BasicDetailsSearchResultsPage.tsx
index 0f7d9a89f4..c813715de0 100644
--- a/packages/cpt-ui/src/pages/BasicDetailsSearchResultsPage.tsx
+++ b/packages/cpt-ui/src/pages/BasicDetailsSearchResultsPage.tsx
@@ -1,8 +1,6 @@
-/* eslint-disable max-len */
-import React, {useEffect, useState} from "react"
+import React, {Fragment, useEffect, useState} from "react"
import {useNavigate, useLocation} from "react-router-dom"
import {
- BackLink,
Table,
Container,
Row,
@@ -18,6 +16,8 @@ import EpsSpinner from "@/components/EpsSpinner"
import PatientNotFoundMessage from "@/components/PatientNotFoundMessage"
import SearchResultsTooManyMessage from "@/components/SearchResultsTooManyMessage"
import {useSearchContext} from "@/context/SearchProvider"
+import {useNavigationContext} from "@/context/NavigationProvider"
+import EpsBackLink from "@/components/EpsBackLink"
import UnknownErrorMessage from "@/components/UnknownErrorMessage"
import axios from "axios"
import {useAuth} from "@/context/AuthProvider"
@@ -29,6 +29,8 @@ export default function SearchResultsPage() {
const [loading, setLoading] = useState(true)
const [patients, setPatients] = useState>([])
const searchContext = useSearchContext()
+ const navigationContext = useNavigationContext()
+
const [error, setError] = useState(false)
const auth = useAuth()
@@ -38,8 +40,8 @@ export default function SearchResultsPage() {
}, [])
const getSearchResults = async () => {
- try{
- // Attempt to fetch live search results from the API
+ try {
+ // Attempt to fetch live search results from the API
const response = await http.get(API_ENDPOINTS.PATIENT_SEARCH, {
params: {
familyName: searchContext.lastName,
@@ -61,8 +63,12 @@ export default function SearchResultsPage() {
}
if (payload.length === 1) {
- searchContext.clearSearchParameters()
- searchContext.setNhsNumber(payload[0].nhsNumber)
+ const relevantParams =
+ navigationContext.getRelevantSearchParameters("basicDetails")
+ searchContext.setAllSearchParameters({
+ ...relevantParams,
+ nhsNumber: payload[0].nhsNumber
+ })
navigate(FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT)
return
}
@@ -70,7 +76,7 @@ export default function SearchResultsPage() {
setPatients(payload)
setLoading(false)
} catch (err) {
- if (axios.isAxiosError(err) && (err.response?.status === 401)) {
+ if (axios.isAxiosError(err) && err.response?.status === 401) {
const invalidSessionCause = err.response?.data?.invalidSessionCause
handleRestartLogin(auth, invalidSessionCause)
return
@@ -82,16 +88,16 @@ export default function SearchResultsPage() {
}
const handleRowClick = (nhsNumber: string) => {
- searchContext.clearSearchParameters()
- searchContext.setNhsNumber(nhsNumber)
+ // only preserve relevant search parameters and add NHS number
+ const relevantParams =
+ navigationContext.getRelevantSearchParameters("basicDetails")
+ searchContext.setAllSearchParameters({
+ ...relevantParams,
+ nhsNumber: nhsNumber
+ })
navigate(`${FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT}`)
}
- // Pass back the query string to keep filled form on return
- const handleGoBack = () => {
- navigate(`${FRONTEND_PATHS.SEARCH_BY_BASIC_DETAILS}`)
- }
-
// Sort by first name
const sortedPatients = patients
.toSorted((a, b) => (a.givenName?.[0] ?? "").localeCompare(b.givenName?.[0] ?? ""))
@@ -118,7 +124,7 @@ export default function SearchResultsPage() {
// Show not found message if no valid patients
if (patients.length === 0) {
- return
+ return
}
// Show too many results message if search returns too many patients
@@ -127,72 +133,82 @@ export default function SearchResultsPage() {
}
return (
-
-
-
-
- e.key === "Enter" && handleGoBack()}
- >
- {SearchResultsPageStrings.GO_BACK}
-
-
-
-
-
-
-
- {SearchResultsPageStrings.RESULTS_COUNT.replace("{count}", sortedPatients.length.toString())}
-
-
+
+
+
+
+
)
}
diff --git a/packages/cpt-ui/src/pages/PrescriptionDetailsPage.tsx b/packages/cpt-ui/src/pages/PrescriptionDetailsPage.tsx
index 8886e724c2..abffee3496 100644
--- a/packages/cpt-ui/src/pages/PrescriptionDetailsPage.tsx
+++ b/packages/cpt-ui/src/pages/PrescriptionDetailsPage.tsx
@@ -1,12 +1,7 @@
import {useEffect, useState} from "react"
-import {Link, useNavigate} from "react-router-dom"
+import {useNavigate} from "react-router-dom"
-import {
- BackLink,
- Col,
- Container,
- Row
-} from "nhsuk-react-components"
+import {Col, Container, Row} from "nhsuk-react-components"
import {
OrgSummary,
@@ -25,11 +20,11 @@ import EpsSpinner from "@/components/EpsSpinner"
import {SiteDetailsCards} from "@/components/prescriptionDetails/SiteDetailsCards"
import {ItemsCards} from "@/components/prescriptionDetails/ItemsCards"
import {MessageHistoryCard} from "@/components/prescriptionDetails/MessageHistoryCard"
+import EpsBackLink from "@/components/EpsBackLink"
import http from "@/helpers/axios"
import {logger} from "@/helpers/logger"
import {useSearchContext} from "@/context/SearchProvider"
-import {buildBackLink, determineSearchType} from "@/helpers/prescriptionNotFoundLinks"
import axios from "axios"
import {handleRestartLogin} from "@/helpers/logout"
import {useAuth} from "@/context/AuthProvider"
@@ -50,9 +45,6 @@ export default function PrescriptionDetailsPage() {
const searchContext = useSearchContext()
const navigate = useNavigate()
- const searchType = determineSearchType(searchContext)
- const backLinkUrl = buildBackLink(searchType, searchContext)
-
const getPrescriptionDetails = async (
prescriptionId: string,
prescriptionIssueNumber?: string | undefined
@@ -105,7 +97,6 @@ export default function PrescriptionDetailsPage() {
useEffect(() => {
const runGetPrescriptionDetails = async () => {
-
const prescriptionId = searchContext.prescriptionId
if (!prescriptionId) {
logger.info("No prescriptionId provided - redirecting to search")
@@ -124,15 +115,14 @@ export default function PrescriptionDetailsPage() {
if (loading) {
return (
-
+
-
- {STRINGS.HEADER}
-
+ {STRINGS.HEADER}
{STRINGS.LOADING_FULL_PRESCRIPTION}
@@ -145,19 +135,13 @@ export default function PrescriptionDetailsPage() {
}
return (
-
-
-
-
-
- {STRINGS.GO_BACK}
-
-
-
+
+
+
{STRINGS.HEADER}
@@ -177,7 +161,7 @@ export default function PrescriptionDetailsPage() {
{/* Message history timeline */}
-
-
+
+
)
}
diff --git a/packages/cpt-ui/src/pages/PrescriptionListPage.tsx b/packages/cpt-ui/src/pages/PrescriptionListPage.tsx
index bc915780b6..091a4e5d2a 100644
--- a/packages/cpt-ui/src/pages/PrescriptionListPage.tsx
+++ b/packages/cpt-ui/src/pages/PrescriptionListPage.tsx
@@ -1,11 +1,6 @@
import React, {Fragment, useEffect, useState} from "react"
-import {Link, useNavigate} from "react-router-dom"
-import {
- BackLink,
- Col,
- Container,
- Row
-} from "nhsuk-react-components"
+import {useNavigate} from "react-router-dom"
+import {Col, Container, Row} from "nhsuk-react-components"
import "../styles/PrescriptionTable.scss"
import axios from "axios"
@@ -17,6 +12,7 @@ import PrescriptionsListTabs from "@/components/prescriptionList/PrescriptionsLi
import {TabHeader} from "@/components/EpsTabs"
import PrescriptionNotFoundMessage from "@/components/PrescriptionNotFoundMessage"
import UnknownErrorMessage from "@/components/UnknownErrorMessage"
+import EpsBackLink from "@/components/EpsBackLink"
import {PRESCRIPTION_LIST_TABS} from "@/constants/ui-strings/PrescriptionListTabStrings"
import {PRESCRIPTION_LIST_PAGE_STRINGS} from "@/constants/ui-strings/PrescriptionListPageStrings"
@@ -27,7 +23,6 @@ import {SearchResponse, PrescriptionSummary} from "@cpt-ui-common/common-types/s
import http from "@/helpers/axios"
import {logger} from "@/helpers/logger"
import {useSearchContext} from "@/context/SearchProvider"
-import {buildBackLink, determineSearchType} from "@/helpers/prescriptionNotFoundLinks"
import {handleRestartLogin, signOut} from "@/helpers/logout"
import {useAuth} from "@/context/AuthProvider"
import {AUTH_CONFIG} from "@/constants/environment"
@@ -45,9 +40,6 @@ export default function PrescriptionListPage() {
const [showNotFound, setShowNotFound] = useState(false)
const [error, setError] = useState(false)
- const searchType = determineSearchType(searchContext)
- const backLinkUrl = buildBackLink(searchType, searchContext)
-
const auth = useAuth()
const navigate = useNavigate()
@@ -59,10 +51,10 @@ export default function PrescriptionListPage() {
const searchParams = new URLSearchParams()
// determine which search page to go back to based on query parameters
- if (searchContext.prescriptionId) {
- searchParams.append("prescriptionId", searchContext.prescriptionId)
- } else if (searchContext.nhsNumber) {
+ if (searchContext.nhsNumber) {
searchParams.append("nhsNumber", searchContext.nhsNumber)
+ } else if (searchContext.prescriptionId) {
+ searchParams.append("prescriptionId", searchContext.prescriptionId)
} else {
logger.info("No search parameter provided - redirecting to prescription ID search")
navigate(FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID)
@@ -91,7 +83,10 @@ export default function PrescriptionListPage() {
searchResults.pastPrescriptions.length === 0 &&
searchResults.futurePrescriptions.length === 0
) {
- logger.warn("A patient was returned, but they do not have any prescriptions.", searchResults)
+ logger.error(
+ "A patient was returned, but they do not have any prescriptions.",
+ searchResults
+ )
setPatientDetails(searchResults.patient)
setShowNotFound(true)
setLoading(false)
@@ -182,22 +177,16 @@ export default function PrescriptionListPage() {
return (
- {PRESCRIPTION_LIST_PAGE_STRINGS.PAGE_TITLE}
+
+ {PRESCRIPTION_LIST_PAGE_STRINGS.PAGE_TITLE}
+
+
-
-
-
-
-