Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/run_regression_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ jobs:
GITHUB-TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
if [[ "$TARGET_ENVIRONMENT" != "prod" && "$TARGET_ENVIRONMENT" != "ref" ]]; then
REGRESSION_TEST_REPO_TAG="v3.8.10" # This is the tag or branch of the regression test code to run, usually a version tag like v3.1.0 or a branch name
REGRESSION_TEST_WORKFLOW_TAG="v3.8.10" # This is the tag of the github workflow to run, usually the same as REGRESSION_TEST_REPO_TAG
REGRESSION_TEST_REPO_TAG="v3.8.20" # This is the tag or branch of the regression test code to run, usually a version tag like v3.1.0 or a branch name
REGRESSION_TEST_WORKFLOW_TAG="v3.8.20" # This is the tag of the github workflow to run, usually the same as REGRESSION_TEST_REPO_TAG


if [[ -z "$REGRESSION_TEST_REPO_TAG" || -z "$REGRESSION_TEST_WORKFLOW_TAG" ]]; then
Expand Down
300 changes: 286 additions & 14 deletions packages/cpt-ui/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import "@testing-library/jest-dom"
import {render, screen} from "@testing-library/react"
import {BrowserRouter} from "react-router-dom"
import
{render,
screen,
fireEvent,
waitFor,
act}
from "@testing-library/react"
import {BrowserRouter, MemoryRouter} from "react-router-dom"
import React from "react"
import App from "@/App"
import {FRONTEND_PATHS} from "@/constants/environment"

// Mock all the context providers
jest.mock("@/context/AuthProvider", () => ({
Expand Down Expand Up @@ -64,19 +71,284 @@ jest.mock("@/pages/SessionLoggedOut", () => () => <div>Session Logged Out</div>)

// Mock EPSCookieBanner
jest.mock("@/components/EPSCookieBanner", () => () => <div>Cookie Banner</div>)
jest.mock("@/pages/TooManySearchResultsPage", () => () => <div>Too Many Search Results</div>)
jest.mock("@/pages/NoPrescriptionsFoundPage", () => () => <div>No Prescriptions Found</div>)
jest.mock("@/pages/NoPatientsFoundPage", () => () => <div>No Patients Found</div>)

// Mock HEADER_STRINGS to avoid dependency on constants
jest.mock("@/constants/ui-strings/HeaderStrings", () => ({
HEADER_STRINGS: {
SKIP_TO_MAIN_CONTENT: "Skip to main content"
}
}))

// Test helper function to render App with specific route
const renderAppAtRoute = (route: string) => {
return render(
<MemoryRouter initialEntries={[route]}>
<App />
</MemoryRouter>
)
}

describe("App", () => {
it("renders the skip link for regular pages", () => {
render(
<BrowserRouter>
<App />
</BrowserRouter>
)

const skipLink = screen.getByTestId("eps_header_skipLink")
expect(skipLink).toBeInTheDocument()
expect(skipLink).toHaveAttribute("href", "#main-content")
expect(skipLink).toHaveTextContent("Skip to main content")
expect(skipLink).toHaveClass("nhsuk-skip-link")
// Setup function to clear focus before each test
beforeEach(() => {
// Reset focus to body
if (document.activeElement && document.activeElement !== document.body) {
(document.activeElement as HTMLElement).blur?.()
}
// Clear any existing event listeners
jest.clearAllMocks()
})

describe("Skip link rendering", () => {
it("renders the skip link for regular pages", () => {
render(
<BrowserRouter>
<App />
</BrowserRouter>
)

const skipLink = screen.getByTestId("eps_header_skipLink")
expect(skipLink).toBeInTheDocument()
expect(skipLink).toHaveAttribute("href", "#main-content")
expect(skipLink).toHaveTextContent("Skip to main content")
expect(skipLink).toHaveClass("nhsuk-skip-link")
})

it("renders skip link with patient details banner target for prescription list current page", () => {
renderAppAtRoute(FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT)

const skipLink = screen.getByTestId("eps_header_skipLink")
expect(skipLink).toHaveAttribute("href", "#patient-details-banner")
})

it("renders skip link with patient details banner target for prescription list future page", () => {
renderAppAtRoute(FRONTEND_PATHS.PRESCRIPTION_LIST_FUTURE)

const skipLink = screen.getByTestId("eps_header_skipLink")
expect(skipLink).toHaveAttribute("href", "#patient-details-banner")
})

it("renders skip link with patient details banner target for prescription list past page", () => {
renderAppAtRoute(FRONTEND_PATHS.PRESCRIPTION_LIST_PAST)

const skipLink = screen.getByTestId("eps_header_skipLink")
expect(skipLink).toHaveAttribute("href", "#patient-details-banner")
})

it("renders skip link with patient details banner target for prescription details page", () => {
renderAppAtRoute(`${FRONTEND_PATHS.PRESCRIPTION_DETAILS_PAGE}/123`)

const skipLink = screen.getByTestId("eps_header_skipLink")
expect(skipLink).toHaveAttribute("href", "#patient-details-banner")
})

it("renders skip link with main content target for non-prescription pages", () => {
renderAppAtRoute(FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID)

const skipLink = screen.getByTestId("eps_header_skipLink")
expect(skipLink).toHaveAttribute("href", "#main-content")
})
})

describe("Skip link keyboard navigation", () => {
it("focuses skip link on first Tab press when page loads without user interaction", async () => {
renderAppAtRoute("/")

const skipLink = screen.getByTestId("eps_header_skipLink")

// Simulate Tab key press
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})

await waitFor(() => {
expect(skipLink).toHaveFocus()
})
})

it("does not focus skip link on Tab press when user has already clicked on page", async () => {
renderAppAtRoute("/")

const skipLink = screen.getByTestId("eps_header_skipLink")

// Simulate user clicking on the page
fireEvent.click(document.body)

// Simulate Tab key press
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})

await waitFor(() => {
expect(skipLink).not.toHaveFocus()
})
})

it("does not focus skip link on Tab press when user has already focused an element", async () => {
renderAppAtRoute("/")

const skipLink = screen.getByTestId("eps_header_skipLink")

// Create a focusable element and simulate user focusing it
const button = document.createElement("button")
button.setAttribute("data-testid", "test-button")
document.body.appendChild(button)
fireEvent.focusIn(button)

// Simulate Tab key press
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})

await waitFor(() => {
expect(skipLink).not.toHaveFocus()
})

// Cleanup
document.body.removeChild(button)
})

it("does not focus skip link on Shift+Tab press", async () => {
renderAppAtRoute("/")

const skipLink = screen.getByTestId("eps_header_skipLink")

// Simulate Shift+Tab key press
fireEvent.keyDown(document, {key: "Tab", code: "Tab", shiftKey: true})

await waitFor(() => {
expect(skipLink).not.toHaveFocus()
})
})

it("does not trigger skip link behavior on non-Tab key press", async () => {
renderAppAtRoute("/")

const skipLink = screen.getByTestId("eps_header_skipLink")

// Simulate Enter key press
fireEvent.keyDown(document, {key: "Enter", code: "Enter"})

await waitFor(() => {
expect(skipLink).not.toHaveFocus()
})
})

it("only triggers skip link behavior once per page load", async () => {
renderAppAtRoute("/")

const skipLink = screen.getByTestId("eps_header_skipLink")

// First Tab press should focus skip link
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
await waitFor(() => {
expect(skipLink).toHaveFocus()
})

// Blur the skip link
skipLink.blur()

// Second Tab press should not focus skip link again
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})

// Give some time to ensure focus doesn't change
await new Promise(resolve => setTimeout(resolve, 50))
expect(skipLink).not.toHaveFocus()
})

it("resets skip link behavior when navigating to a new page", async () => {
const {rerender} = render(
<MemoryRouter initialEntries={["/"]}>
<App />
</MemoryRouter>
)

let skipLink = screen.getByTestId("eps_header_skipLink")

// First Tab press should focus skip link
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
await waitFor(() => {
expect(skipLink).toHaveFocus()
})

// Navigate to a new page by re-rendering with a different route
await act(async () => {
rerender(
<MemoryRouter initialEntries={[FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID]}>
<App />
</MemoryRouter>
)
})

// Get the new skip link element after rerender
skipLink = screen.getByTestId("eps_header_skipLink")

// Tab press should focus skip link again after navigation
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
await waitFor(() => {
expect(skipLink).toHaveFocus()
})
})

it("handles case when skip link element is not found", async () => {
renderAppAtRoute("/")

// Mock querySelector to return null
const originalQuerySelector = document.querySelector
document.querySelector = jest.fn().mockReturnValue(null)

// This should not throw an error
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})

// Restore original querySelector
document.querySelector = originalQuerySelector
})

it("detects user interaction when an element already has focus on page load", async () => {
// Create a focusable element and focus it before rendering
const input = document.createElement("input")
input.setAttribute("data-testid", "pre-focused-input")
document.body.appendChild(input)
input.focus()

renderAppAtRoute("/")

const skipLink = screen.getByTestId("eps_header_skipLink")

// Tab press should not focus skip link since user has already interacted
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})

await waitFor(() => {
expect(skipLink).not.toHaveFocus()
})

// Cleanup
document.body.removeChild(input)
})
})

describe("Route handling", () => {
it("renders the app with routing components", () => {
render(
<BrowserRouter>
<App />
</BrowserRouter>
)

// Verify the basic app structure is rendered
expect(screen.getByTestId("eps_header_skipLink")).toBeInTheDocument()
expect(screen.getByTestId("layout")).toBeInTheDocument()
})

it("handles different route paths without errors", () => {
expect(() => {
render(
<MemoryRouter initialEntries={["/login"]}>
<App />
</MemoryRouter>
)
}).not.toThrow()

expect(screen.getByTestId("eps_header_skipLink")).toBeInTheDocument()
})
})
})

// Removed duplicate describe block
20 changes: 16 additions & 4 deletions packages/cpt-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,43 @@ import {HEADER_STRINGS} from "@/constants/ui-strings/HeaderStrings"
function AppContent() {
const location = useLocation()

// this useEffect ensures that focus starts with skip link when using tab navigation
// this useEffect ensures that focus starts with skip link when using tab navigation,
// unless the user has already interacted with the page
useEffect(() => {
let hasTabbed = false
let hasUserInteracted = false

const activeElement = document.activeElement as HTMLElement
if (activeElement && activeElement !== document.body) {
activeElement.blur()
if (activeElement && activeElement !== document.body && activeElement.tagName !== "HTML") {
hasUserInteracted = true
}

const handleUserInteraction = () => {
hasUserInteracted = true
}

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Tab" && !hasTabbed && !e.shiftKey) {
if (e.key === "Tab" && !hasTabbed && !hasUserInteracted && !e.shiftKey) {
hasTabbed = true
e.preventDefault()
const skipLink = document.querySelector(".nhsuk-skip-link") as HTMLElement
if (skipLink) {
skipLink.focus()
}
document.removeEventListener("keydown", handleKeyDown)
document.removeEventListener("click", handleUserInteraction)
document.removeEventListener("focusin", handleUserInteraction)
}
}

document.addEventListener("click", handleUserInteraction)
document.addEventListener("focusin", handleUserInteraction)
document.addEventListener("keydown", handleKeyDown)

return () => {
document.removeEventListener("keydown", handleKeyDown)
document.removeEventListener("click", handleUserInteraction)
document.removeEventListener("focusin", handleUserInteraction)
}
}, [location.pathname])

Expand Down
Loading