Skip to content

Commit 264a8e8

Browse files
Fix: [AEA-5935] - privacy notice (#1713)
## Summary https://nhsd-jira.digital.nhs.uk/browse/AEA-5935 - Routine Change ### Details changed hyphens to en dashes made it so that interacting with the page can affect the tabbing order for skip link
1 parent 8af4fd8 commit 264a8e8

File tree

4 files changed

+315
-31
lines changed

4 files changed

+315
-31
lines changed

.github/workflows/run_regression_tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ jobs:
7373
GITHUB-TOKEN: ${{ steps.generate-token.outputs.token }}
7474
run: |
7575
if [[ "$TARGET_ENVIRONMENT" != "prod" && "$TARGET_ENVIRONMENT" != "ref" ]]; then
76-
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
77-
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
76+
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
77+
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
7878
7979
8080
if [[ -z "$REGRESSION_TEST_REPO_TAG" || -z "$REGRESSION_TEST_WORKFLOW_TAG" ]]; then

packages/cpt-ui/__tests__/App.test.tsx

Lines changed: 286 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import "@testing-library/jest-dom"
2-
import {render, screen} from "@testing-library/react"
3-
import {BrowserRouter} from "react-router-dom"
2+
import
3+
{render,
4+
screen,
5+
fireEvent,
6+
waitFor,
7+
act}
8+
from "@testing-library/react"
9+
import {BrowserRouter, MemoryRouter} from "react-router-dom"
410
import React from "react"
511
import App from "@/App"
12+
import {FRONTEND_PATHS} from "@/constants/environment"
613

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

6572
// Mock EPSCookieBanner
6673
jest.mock("@/components/EPSCookieBanner", () => () => <div>Cookie Banner</div>)
74+
jest.mock("@/pages/TooManySearchResultsPage", () => () => <div>Too Many Search Results</div>)
75+
jest.mock("@/pages/NoPrescriptionsFoundPage", () => () => <div>No Prescriptions Found</div>)
76+
jest.mock("@/pages/NoPatientsFoundPage", () => () => <div>No Patients Found</div>)
77+
78+
// Mock HEADER_STRINGS to avoid dependency on constants
79+
jest.mock("@/constants/ui-strings/HeaderStrings", () => ({
80+
HEADER_STRINGS: {
81+
SKIP_TO_MAIN_CONTENT: "Skip to main content"
82+
}
83+
}))
84+
85+
// Test helper function to render App with specific route
86+
const renderAppAtRoute = (route: string) => {
87+
return render(
88+
<MemoryRouter initialEntries={[route]}>
89+
<App />
90+
</MemoryRouter>
91+
)
92+
}
6793

6894
describe("App", () => {
69-
it("renders the skip link for regular pages", () => {
70-
render(
71-
<BrowserRouter>
72-
<App />
73-
</BrowserRouter>
74-
)
75-
76-
const skipLink = screen.getByTestId("eps_header_skipLink")
77-
expect(skipLink).toBeInTheDocument()
78-
expect(skipLink).toHaveAttribute("href", "#main-content")
79-
expect(skipLink).toHaveTextContent("Skip to main content")
80-
expect(skipLink).toHaveClass("nhsuk-skip-link")
95+
// Setup function to clear focus before each test
96+
beforeEach(() => {
97+
// Reset focus to body
98+
if (document.activeElement && document.activeElement !== document.body) {
99+
(document.activeElement as HTMLElement).blur?.()
100+
}
101+
// Clear any existing event listeners
102+
jest.clearAllMocks()
103+
})
104+
105+
describe("Skip link rendering", () => {
106+
it("renders the skip link for regular pages", () => {
107+
render(
108+
<BrowserRouter>
109+
<App />
110+
</BrowserRouter>
111+
)
112+
113+
const skipLink = screen.getByTestId("eps_header_skipLink")
114+
expect(skipLink).toBeInTheDocument()
115+
expect(skipLink).toHaveAttribute("href", "#main-content")
116+
expect(skipLink).toHaveTextContent("Skip to main content")
117+
expect(skipLink).toHaveClass("nhsuk-skip-link")
118+
})
119+
120+
it("renders skip link with patient details banner target for prescription list current page", () => {
121+
renderAppAtRoute(FRONTEND_PATHS.PRESCRIPTION_LIST_CURRENT)
122+
123+
const skipLink = screen.getByTestId("eps_header_skipLink")
124+
expect(skipLink).toHaveAttribute("href", "#patient-details-banner")
125+
})
126+
127+
it("renders skip link with patient details banner target for prescription list future page", () => {
128+
renderAppAtRoute(FRONTEND_PATHS.PRESCRIPTION_LIST_FUTURE)
129+
130+
const skipLink = screen.getByTestId("eps_header_skipLink")
131+
expect(skipLink).toHaveAttribute("href", "#patient-details-banner")
132+
})
133+
134+
it("renders skip link with patient details banner target for prescription list past page", () => {
135+
renderAppAtRoute(FRONTEND_PATHS.PRESCRIPTION_LIST_PAST)
136+
137+
const skipLink = screen.getByTestId("eps_header_skipLink")
138+
expect(skipLink).toHaveAttribute("href", "#patient-details-banner")
139+
})
140+
141+
it("renders skip link with patient details banner target for prescription details page", () => {
142+
renderAppAtRoute(`${FRONTEND_PATHS.PRESCRIPTION_DETAILS_PAGE}/123`)
143+
144+
const skipLink = screen.getByTestId("eps_header_skipLink")
145+
expect(skipLink).toHaveAttribute("href", "#patient-details-banner")
146+
})
147+
148+
it("renders skip link with main content target for non-prescription pages", () => {
149+
renderAppAtRoute(FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID)
150+
151+
const skipLink = screen.getByTestId("eps_header_skipLink")
152+
expect(skipLink).toHaveAttribute("href", "#main-content")
153+
})
154+
})
155+
156+
describe("Skip link keyboard navigation", () => {
157+
it("focuses skip link on first Tab press when page loads without user interaction", async () => {
158+
renderAppAtRoute("/")
159+
160+
const skipLink = screen.getByTestId("eps_header_skipLink")
161+
162+
// Simulate Tab key press
163+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
164+
165+
await waitFor(() => {
166+
expect(skipLink).toHaveFocus()
167+
})
168+
})
169+
170+
it("does not focus skip link on Tab press when user has already clicked on page", async () => {
171+
renderAppAtRoute("/")
172+
173+
const skipLink = screen.getByTestId("eps_header_skipLink")
174+
175+
// Simulate user clicking on the page
176+
fireEvent.click(document.body)
177+
178+
// Simulate Tab key press
179+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
180+
181+
await waitFor(() => {
182+
expect(skipLink).not.toHaveFocus()
183+
})
184+
})
185+
186+
it("does not focus skip link on Tab press when user has already focused an element", async () => {
187+
renderAppAtRoute("/")
188+
189+
const skipLink = screen.getByTestId("eps_header_skipLink")
190+
191+
// Create a focusable element and simulate user focusing it
192+
const button = document.createElement("button")
193+
button.setAttribute("data-testid", "test-button")
194+
document.body.appendChild(button)
195+
fireEvent.focusIn(button)
196+
197+
// Simulate Tab key press
198+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
199+
200+
await waitFor(() => {
201+
expect(skipLink).not.toHaveFocus()
202+
})
203+
204+
// Cleanup
205+
document.body.removeChild(button)
206+
})
207+
208+
it("does not focus skip link on Shift+Tab press", async () => {
209+
renderAppAtRoute("/")
210+
211+
const skipLink = screen.getByTestId("eps_header_skipLink")
212+
213+
// Simulate Shift+Tab key press
214+
fireEvent.keyDown(document, {key: "Tab", code: "Tab", shiftKey: true})
215+
216+
await waitFor(() => {
217+
expect(skipLink).not.toHaveFocus()
218+
})
219+
})
220+
221+
it("does not trigger skip link behavior on non-Tab key press", async () => {
222+
renderAppAtRoute("/")
223+
224+
const skipLink = screen.getByTestId("eps_header_skipLink")
225+
226+
// Simulate Enter key press
227+
fireEvent.keyDown(document, {key: "Enter", code: "Enter"})
228+
229+
await waitFor(() => {
230+
expect(skipLink).not.toHaveFocus()
231+
})
232+
})
233+
234+
it("only triggers skip link behavior once per page load", async () => {
235+
renderAppAtRoute("/")
236+
237+
const skipLink = screen.getByTestId("eps_header_skipLink")
238+
239+
// First Tab press should focus skip link
240+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
241+
await waitFor(() => {
242+
expect(skipLink).toHaveFocus()
243+
})
244+
245+
// Blur the skip link
246+
skipLink.blur()
247+
248+
// Second Tab press should not focus skip link again
249+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
250+
251+
// Give some time to ensure focus doesn't change
252+
await new Promise(resolve => setTimeout(resolve, 50))
253+
expect(skipLink).not.toHaveFocus()
254+
})
255+
256+
it("resets skip link behavior when navigating to a new page", async () => {
257+
const {rerender} = render(
258+
<MemoryRouter initialEntries={["/"]}>
259+
<App />
260+
</MemoryRouter>
261+
)
262+
263+
let skipLink = screen.getByTestId("eps_header_skipLink")
264+
265+
// First Tab press should focus skip link
266+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
267+
await waitFor(() => {
268+
expect(skipLink).toHaveFocus()
269+
})
270+
271+
// Navigate to a new page by re-rendering with a different route
272+
await act(async () => {
273+
rerender(
274+
<MemoryRouter initialEntries={[FRONTEND_PATHS.SEARCH_BY_PRESCRIPTION_ID]}>
275+
<App />
276+
</MemoryRouter>
277+
)
278+
})
279+
280+
// Get the new skip link element after rerender
281+
skipLink = screen.getByTestId("eps_header_skipLink")
282+
283+
// Tab press should focus skip link again after navigation
284+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
285+
await waitFor(() => {
286+
expect(skipLink).toHaveFocus()
287+
})
288+
})
289+
290+
it("handles case when skip link element is not found", async () => {
291+
renderAppAtRoute("/")
292+
293+
// Mock querySelector to return null
294+
const originalQuerySelector = document.querySelector
295+
document.querySelector = jest.fn().mockReturnValue(null)
296+
297+
// This should not throw an error
298+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
299+
300+
// Restore original querySelector
301+
document.querySelector = originalQuerySelector
302+
})
303+
304+
it("detects user interaction when an element already has focus on page load", async () => {
305+
// Create a focusable element and focus it before rendering
306+
const input = document.createElement("input")
307+
input.setAttribute("data-testid", "pre-focused-input")
308+
document.body.appendChild(input)
309+
input.focus()
310+
311+
renderAppAtRoute("/")
312+
313+
const skipLink = screen.getByTestId("eps_header_skipLink")
314+
315+
// Tab press should not focus skip link since user has already interacted
316+
fireEvent.keyDown(document, {key: "Tab", code: "Tab"})
317+
318+
await waitFor(() => {
319+
expect(skipLink).not.toHaveFocus()
320+
})
321+
322+
// Cleanup
323+
document.body.removeChild(input)
324+
})
325+
})
326+
327+
describe("Route handling", () => {
328+
it("renders the app with routing components", () => {
329+
render(
330+
<BrowserRouter>
331+
<App />
332+
</BrowserRouter>
333+
)
334+
335+
// Verify the basic app structure is rendered
336+
expect(screen.getByTestId("eps_header_skipLink")).toBeInTheDocument()
337+
expect(screen.getByTestId("layout")).toBeInTheDocument()
338+
})
339+
340+
it("handles different route paths without errors", () => {
341+
expect(() => {
342+
render(
343+
<MemoryRouter initialEntries={["/login"]}>
344+
<App />
345+
</MemoryRouter>
346+
)
347+
}).not.toThrow()
348+
349+
expect(screen.getByTestId("eps_header_skipLink")).toBeInTheDocument()
350+
})
81351
})
82352
})
353+
354+
// Removed duplicate describe block

packages/cpt-ui/src/App.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,43 @@ import {HEADER_STRINGS} from "@/constants/ui-strings/HeaderStrings"
3434
function AppContent() {
3535
const location = useLocation()
3636

37-
// this useEffect ensures that focus starts with skip link when using tab navigation
37+
// this useEffect ensures that focus starts with skip link when using tab navigation,
38+
// unless the user has already interacted with the page
3839
useEffect(() => {
3940
let hasTabbed = false
41+
let hasUserInteracted = false
4042

4143
const activeElement = document.activeElement as HTMLElement
42-
if (activeElement && activeElement !== document.body) {
43-
activeElement.blur()
44+
if (activeElement && activeElement !== document.body && activeElement.tagName !== "HTML") {
45+
hasUserInteracted = true
46+
}
47+
48+
const handleUserInteraction = () => {
49+
hasUserInteracted = true
4450
}
4551

4652
const handleKeyDown = (e: KeyboardEvent) => {
47-
if (e.key === "Tab" && !hasTabbed && !e.shiftKey) {
53+
if (e.key === "Tab" && !hasTabbed && !hasUserInteracted && !e.shiftKey) {
4854
hasTabbed = true
4955
e.preventDefault()
5056
const skipLink = document.querySelector(".nhsuk-skip-link") as HTMLElement
5157
if (skipLink) {
5258
skipLink.focus()
5359
}
5460
document.removeEventListener("keydown", handleKeyDown)
61+
document.removeEventListener("click", handleUserInteraction)
62+
document.removeEventListener("focusin", handleUserInteraction)
5563
}
5664
}
5765

66+
document.addEventListener("click", handleUserInteraction)
67+
document.addEventListener("focusin", handleUserInteraction)
5868
document.addEventListener("keydown", handleKeyDown)
5969

6070
return () => {
6171
document.removeEventListener("keydown", handleKeyDown)
72+
document.removeEventListener("click", handleUserInteraction)
73+
document.removeEventListener("focusin", handleUserInteraction)
6274
}
6375
}, [location.pathname])
6476

0 commit comments

Comments
 (0)