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
216 changes: 216 additions & 0 deletions packages/cpt-ui/__tests__/AccessibilityStatementPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import "@testing-library/jest-dom"
import React, {useState} from "react"
import {render, screen} from "@testing-library/react"
import {MemoryRouter} from "react-router-dom"
import {AuthContext, type AuthContextType} from "@/context/AuthProvider"
import AccessibilityStatementPage from "@/pages/AccessibilityStatementPage"
import {AccessibilityStatementStrings} from "@/constants/ui-strings/AccessibilityStatementStrings"
import {mockAuthState} from "./mocks/AuthStateMock"

jest.mock("@/helpers/awsRum")
jest.mock("@/context/configureAmplify")

jest.mock("@/constants/environment", () => ({
AUTH_CONFIG: {
USER_POOL_ID: "test-pool-id",
USER_POOL_CLIENT_ID: "test-client-id",
HOSTED_LOGIN_DOMAIN: "test.domain",
REDIRECT_SIGN_IN: "http://localhost:3000",
REDIRECT_SIGN_OUT: "http://localhost:3000/logout"
},
APP_CONFIG: {
REACT_LOG_LEVEL: "debug"
},
API_ENDPOINTS: {
CIS2_SIGNOUT_ENDPOINT: "/api/cis2-signout"
},
FRONTEND_PATHS: {
LOGIN: "/login",
SEARCH_BY_PRESCRIPTION_ID: "/search-by-prescription-id"
}
}))

const signedOutAuthState: AuthContextType = {
...mockAuthState,
isSignedIn: false
}

const signedInAuthState: AuthContextType = {
...mockAuthState,
isSignedIn: true
}

const MockAuthProvider = ({
children,
authState
}: {
children: React.ReactNode;
authState: AuthContextType;
}) => {
const [state] = useState<AuthContextType>(authState)
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>
}

const renderPage = (authState: AuthContextType = signedOutAuthState) => {
return render(
<MockAuthProvider authState={authState}>
<MemoryRouter>
<AccessibilityStatementPage />
</MemoryRouter>
</MockAuthProvider>
)
}

describe("AccessibilityStatementPage", () => {
describe("page structure", () => {
it("renders the main container", () => {
renderPage()
expect(screen.getByRole("main")).toBeInTheDocument()
})

it("renders the page heading", () => {
renderPage()
expect(
screen.getByRole("heading", {level: 1, name: AccessibilityStatementStrings.HEADER})
).toBeInTheDocument()
})

it("renders the breadcrumb Home link", () => {
renderPage()
expect(screen.getByRole("link", {name: AccessibilityStatementStrings.HOME})).toBeInTheDocument()
})
})

describe("section headings", () => {
beforeEach(() => {
renderPage()
})

it("renders the known issues heading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.KNOWN_ISSUES.HEADER})
).toBeInTheDocument()
})

it("renders the feedback and contact information heading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.FEEDBACK_CONTACT_INFORMATION.HEADER})
).toBeInTheDocument()
})

it("renders the enforcement procedure heading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.ENFORCEMENT_PROCEDURE.HEADER})
).toBeInTheDocument()
})

it("renders the technical information heading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.TECHNICAL_INFORMATION.HEADER})
).toBeInTheDocument()
})

it("renders the compliance status heading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.COMPLIANCE_STATUS.HEADER})
).toBeInTheDocument()
})

it("renders the non-accessible content heading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.NONACCESSIBLE_CONTENT.HEADER})
).toBeInTheDocument()
})

it("renders the non-compliance subheading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.NONACCESSIBLE_CONTENT.SUBHEADER})
).toBeInTheDocument()
})

it("renders the improving accessibility heading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.IMPROVING_ACCESSIBILITY.HEADER})
).toBeInTheDocument()
})

it("renders the preparation subheading", () => {
expect(
screen.getByRole("heading", {name: AccessibilityStatementStrings.IMPROVING_ACCESSIBILITY.SUBHEADER})
).toBeInTheDocument()
})
})

describe("links rendered via EpsRichText", () => {
beforeEach(() => {
renderPage()
})

it("renders the Prescription Tracker link in the opening paragraph", () => {
const link = screen.getByRole("link", {name: "Prescription Tracker"})
expect(link).toHaveAttribute("href", "/site")
expect(link).not.toHaveAttribute("target")
})

it("renders the AbilityNet external link", () => {
const link = screen.getByRole("link", {name: "AbilityNet (opens in new tab)"})
expect(link).toHaveAttribute("href", "https://www.abilitynet.org.uk/")
expect(link).toHaveAttribute("target", "_blank")
expect(link).toHaveAttribute("rel", "noreferrer")
})

it("renders the email link in the feedback section", () => {
const links = screen.getAllByRole("link", {name: "epssupport@nhs.net"})
expect(links).toHaveLength(2)
links.forEach((link) => {
expect(link).toHaveAttribute("href", "mailto:epssupport@nhs.net")
})
})

it("renders the Equality Advisory Service external link", () => {
const link = screen.getByRole("link", {
name: "Equality Advisory and Support Service (opens in new tab)"
})
expect(link).toHaveAttribute("href", "https://www.equalityadvisoryservice.com/")
expect(link).toHaveAttribute("target", "_blank")
expect(link).toHaveAttribute("rel", "noreferrer")
})
})

describe("list content", () => {
it("renders the opening section accessibility features list", () => {
renderPage()
AccessibilityStatementStrings.OPENING_SECTION.LIST_ITEMS.forEach((item) => {
expect(screen.getByText(item)).toBeInTheDocument()
})
})

it("renders the known issues list", () => {
renderPage()
AccessibilityStatementStrings.KNOWN_ISSUES.LIST_ITEMS.forEach((item) => {
expect(screen.getByText(item)).toBeInTheDocument()
})
})

it("renders the non-compliance sub-list", () => {
renderPage()
AccessibilityStatementStrings.NONACCESSIBLE_CONTENT.SUB_LIST_ITEMS.forEach((item) => {
expect(screen.getByText(item.trim())).toBeInTheDocument()
})
})
})

describe("breadcrumb home link destination", () => {
it("links to the login page when signed out", () => {
renderPage(signedOutAuthState)
const homeLink = screen.getByRole("link", {name: AccessibilityStatementStrings.HOME})
expect(homeLink).toHaveAttribute("href", "/login")
})

it("links to the search page when signed in", () => {
renderPage(signedInAuthState)
const homeLink = screen.getByRole("link", {name: AccessibilityStatementStrings.HOME})
expect(homeLink).toHaveAttribute("href", "/search-by-prescription-id")
})
})
})
93 changes: 93 additions & 0 deletions packages/cpt-ui/__tests__/EpsRichText.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import "@testing-library/jest-dom"
import React from "react"
import {render, screen} from "@testing-library/react"
import EpsRichText from "@/components/EpsRichText"
import type {RichTextNode} from "@/components/EpsRichText"

describe("EpsRichText", () => {
describe("plain string content", () => {
it("renders a single string", () => {
render(<EpsRichText content="Hello world" />)
expect(screen.getByText("Hello world")).toBeInTheDocument()
})

it("renders multiple strings from an array", () => {
const {container} = render(<EpsRichText content={["Hello ", "world"]} />)
expect(container).toHaveTextContent("Hello world")
})
})

describe("link node content", () => {
it("renders a single internal link node", () => {
const node: RichTextNode = {text: "Prescription Tracker", href: "/site"}
render(<EpsRichText content={node} />)
const link = screen.getByRole("link", {name: "Prescription Tracker"})
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute("href", "/site")
expect(link).not.toHaveAttribute("target")
expect(link).not.toHaveAttribute("rel")
})

it("renders an external link with target and rel attributes", () => {
const node: RichTextNode = {
text: "AbilityNet (opens in new tab)",
href: "https://www.abilitynet.org.uk/",
external: true
}
render(<EpsRichText content={node} />)
const link = screen.getByRole("link", {name: "AbilityNet (opens in new tab)"})
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute("href", "https://www.abilitynet.org.uk/")
expect(link).toHaveAttribute("target", "_blank")
expect(link).toHaveAttribute("rel", "noreferrer")
})

it("does not add target/rel when external is false", () => {
const node: RichTextNode = {text: "Click here", href: "/somewhere", external: false}
render(<EpsRichText content={node} />)
const link = screen.getByRole("link", {name: "Click here"})
expect(link).not.toHaveAttribute("target")
expect(link).not.toHaveAttribute("rel")
})
})

describe("mixed array content", () => {
it("renders a mix of strings and link nodes", () => {
const content: Array<RichTextNode> = [
"This applies to the ",
{text: "Prescription Tracker", href: "/site"},
"."
]
const {container} = render(<EpsRichText content={content} />)
expect(container).toHaveTextContent(/This applies to the/)
expect(container).toHaveTextContent(/\.$/)
const link = screen.getByRole("link", {name: "Prescription Tracker"})
expect(link).toHaveAttribute("href", "/site")
})

it("renders multiple links in one array", () => {
const content: Array<RichTextNode> = [
{text: "First link", href: "/first"},
" and ",
{text: "Second link", href: "/second", external: true}
]
render(<EpsRichText content={content} />)
const first = screen.getByRole("link", {name: "First link"})
const second = screen.getByRole("link", {name: "Second link"})
expect(first).toHaveAttribute("href", "/first")
expect(first).not.toHaveAttribute("target")
expect(second).toHaveAttribute("href", "/second")
expect(second).toHaveAttribute("target", "_blank")
})

it("renders a mailto link correctly", () => {
const content: Array<RichTextNode> = [
"Contact us at ",
{text: "epssupport@nhs.net", href: "mailto:epssupport@nhs.net"}
]
render(<EpsRichText content={content} />)
const link = screen.getByRole("link", {name: "epssupport@nhs.net"})
expect(link).toHaveAttribute("href", "mailto:epssupport@nhs.net")
})
})
})
2 changes: 2 additions & 0 deletions packages/cpt-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import PrivacyNoticePage from "./pages/PrivacyNoticePage"
import SessionSelectionPage from "./pages/SessionSelection"
import NoPrescriptionsFoundPage from "@/pages/NoPrescriptionsFoundPage"
import NoPatientsFoundPage from "@/pages/NoPatientsFoundPage"
import AccessibilityStatementPage from "./pages/AccessibilityStatementPage"

import {FRONTEND_PATHS} from "@/constants/environment"
import SessionLoggedOutPage from "./pages/SessionLoggedOut"
Expand Down Expand Up @@ -106,6 +107,7 @@ function AppContent() {
<Route path={FRONTEND_PATHS.NO_PATIENT_FOUND} element={<NoPatientsFoundPage />} />
<Route path={FRONTEND_PATHS.NO_PRESCRIPTIONS_FOUND} element={<NoPrescriptionsFoundPage />} />
<Route path={FRONTEND_PATHS.PRIVACY_NOTICE} element={<PrivacyNoticePage />} />
<Route path={FRONTEND_PATHS.ACCESSIBILITY_STATEMENT} element={<AccessibilityStatementPage />} />
</Route>
</Routes>
</NavigationProvider>
Expand Down
35 changes: 35 additions & 0 deletions packages/cpt-ui/src/components/EpsRichText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react"

export type RichTextLinkNode = {
text: string
href: string
external?: boolean
}

export type RichTextNode = string | RichTextLinkNode
export type RichTextContent = RichTextNode | Array<RichTextNode>

interface EpsRichTextProps {
content: RichTextContent
}

export default function EpsRichText({content}: EpsRichTextProps) {

Check warning on line 16 in packages/cpt-ui/src/components/EpsRichText.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=NHSDigital_eps-prescription-tracker-ui&issues=AZ0go7tEXafjTJA5eJte&open=AZ0go7tEXafjTJA5eJte&pullRequest=1949
const nodes = Array.isArray(content) ? content : [content]
return (
<>
{nodes.map((node, i) =>
typeof node === "string" ? (
<React.Fragment key={i}>{node}</React.Fragment>

Check warning on line 22 in packages/cpt-ui/src/components/EpsRichText.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=NHSDigital_eps-prescription-tracker-ui&issues=AZ0go7tEXafjTJA5eJtf&open=AZ0go7tEXafjTJA5eJtf&pullRequest=1949
) : (
<a
key={i}

Check warning on line 25 in packages/cpt-ui/src/components/EpsRichText.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=NHSDigital_eps-prescription-tracker-ui&issues=AZ0go7tEXafjTJA5eJtg&open=AZ0go7tEXafjTJA5eJtg&pullRequest=1949
href={node.href}
{...(node.external ? {target: "_blank", rel: "noreferrer"} : {})}

Check warning on line 27 in packages/cpt-ui/src/components/EpsRichText.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=NHSDigital_eps-prescription-tracker-ui&issues=AZ0go7tEXafjTJA5eJth&open=AZ0go7tEXafjTJA5eJth&pullRequest=1949
>
{node.text}
</a>
)
)}
</>
)
}
2 changes: 2 additions & 0 deletions packages/cpt-ui/src/constants/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const FRONTEND_PATHS = {
NO_PATIENT_FOUND: "/no-patient-found",
NO_PRESCRIPTIONS_FOUND: "/no-prescriptions-found",
PRIVACY_NOTICE: "/privacy-notice",
ACCESSIBILITY_STATEMENT: "/accessibility-statement",
COOKIES_SELECTED: "/cookies-selected",
SESSION_SELECTION: "/select-active-session",
NOT_FOUND: "/notfound"
Expand All @@ -89,6 +90,7 @@ export const PUBLIC_PATHS = [
FRONTEND_PATHS.PRIVACY_NOTICE,
FRONTEND_PATHS.COOKIES_SELECTED,
FRONTEND_PATHS.NOT_FOUND,
FRONTEND_PATHS.ACCESSIBILITY_STATEMENT,
"/"
] as const

Expand Down
Loading
Loading