From 75e814290a5e6a87bb8c1712f9753dd3c71e4f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 14:05:11 +0100 Subject: [PATCH 1/9] refactored the method to split bigg method to separated --- ui/src/routes/HomeTestTermsOfUsePage.tsx | 212 ++++++++++++----------- 1 file changed, 108 insertions(+), 104 deletions(-) diff --git a/ui/src/routes/HomeTestTermsOfUsePage.tsx b/ui/src/routes/HomeTestTermsOfUsePage.tsx index b366562e5..8f4f08421 100644 --- a/ui/src/routes/HomeTestTermsOfUsePage.tsx +++ b/ui/src/routes/HomeTestTermsOfUsePage.tsx @@ -1,89 +1,123 @@ "use client"; +import "@/styles/lists.css"; + import { useNavigate } from "react-router-dom"; + +import type { PrivacyPolicySection, PrivacyPolicySubsection } from "@/content"; import { useContent } from "@/hooks"; import PageLayout from "@/layouts/PageLayout"; -import { renderTextWithLinks, cleanListItems, getListClass } from "@/utils/renderTextWithLinks"; -import "@/styles/lists.css"; +import { cleanListItems, getListClass, renderTextWithLinks } from "@/utils/renderTextWithLinks"; -export default function HomeTestTermsOfUsePage() { - const navigate = useNavigate(); - const { "home-test-terms-of-use": content } = useContent(); +const renderTableCellLines = (cell: string, rowIdx: number, cellIdx: number) => + cell.split("\n").map((line, lineIdx) => ( +

+ {renderTextWithLinks(line, `tbl-${rowIdx}-${cellIdx}-${lineIdx}-`)} +

+ )); + +const renderTableCell = (cell: string, cellIdx: number, rowIdx: number) => ( + + {renderTableCellLines(cell, rowIdx, cellIdx)} + +); - /** - * Renders a paragraph, auto-bolding the leading paragraph reference number (e.g. "1.1. ") - */ - const renderParagraphs = (paragraphs: string[]) => { - return paragraphs.map((paragraph, index) => { - const numberMatch = paragraph.match(/^(\d+\.\d+\.?\s+)/); - if (numberMatch) { - const number = numberMatch[1]; - const rest = paragraph.slice(number.length); - return ( -

- {number} - {renderTextWithLinks(rest, `p${index}-`)} -

- ); - } +const renderTableRow = (row: string[], rowIdx: number) => ( + + {row.map((cell, cellIdx) => renderTableCell(cell, cellIdx, rowIdx))} + +); + +/** + * Renders a paragraph, auto-bolding the leading paragraph reference number (e.g. "1.1. ") + */ +const renderParagraphs = (paragraphs: string[]) => + paragraphs.map((paragraph, index) => { + const numberMatch = paragraph.match(/^(\d+\.\d+\.?\s+)/); + if (numberMatch) { + const number = numberMatch[1]; + const rest = paragraph.slice(number.length); return (

- {renderTextWithLinks(paragraph, `p${index}-`)} + {number} + {renderTextWithLinks(rest, `p${index}-`)}

); - }); - }; - - const renderList = ( - items: string[], - ordered?: boolean, - indented?: boolean, - listStyle?: "bullet" | "dash", - ) => { - const cleanedItems = cleanListItems(items); - const ListTag = ordered ? "ol" : "ul"; - const listClass = getListClass(ordered, listStyle); - const list = ( - - {cleanedItems.map((item, index) => ( -
  • {renderTextWithLinks(item, `li${index}-`)}
  • - ))} -
    - ); - return indented ?
    {list}
    : list; - }; - - const renderTable = (table: { caption?: string; headers: string[]; rows: string[][] }) => { + } return ( - - {table.caption && } - - - {table.headers.map((header, i) => ( - - ))} - - - - {table.rows.map((row, rowIdx) => ( - - {row.map((cell, cellIdx) => ( - - ))} - - ))} - -
    {table.caption}
    - {header} -
    - {cell.split("\n").map((line, lineIdx) => ( -

    - {renderTextWithLinks(line, `tbl-${rowIdx}-${cellIdx}-${lineIdx}-`)} -

    - ))} -
    +

    + {renderTextWithLinks(paragraph, `p${index}-`)} +

    ); - }; + }); + +const renderList = ( + items: string[], + ordered?: boolean, + indented?: boolean, + listStyle?: "bullet" | "dash", +) => { + const cleanedItems = cleanListItems(items); + const ListTag = ordered ? "ol" : "ul"; + const listClass = getListClass(ordered, listStyle); + const list = ( + + {cleanedItems.map((item, index) => ( +
  • {renderTextWithLinks(item, `li${index}-`)}
  • + ))} +
    + ); + return indented ?
    {list}
    : list; +}; + +const renderTable = (table: { caption?: string; headers: string[]; rows: string[][] }) => ( + + {table.caption && } + + + {table.headers.map((header, i) => ( + + ))} + + + {table.rows.map(renderTableRow)} +
    {table.caption}
    + {header} +
    +); + +const renderSubsection = (subsection: PrivacyPolicySubsection, subIndex: number) => ( +
    + {subsection.heading &&

    {subsection.heading}

    } + + {subsection.paragraphs && renderParagraphs(subsection.paragraphs)} + + {subsection.list && + renderList(subsection.list, subsection.ordered, subsection.indented, subsection.listStyle)} + + {subsection.table && renderTable(subsection.table)} +
    +); + +const renderSection = (section: PrivacyPolicySection) => ( +
    +

    + {section.heading} +

    + + {renderParagraphs(section.paragraphs)} + + {section.subsections?.map(renderSubsection)} +
    +); + +export default function HomeTestTermsOfUsePage() { + const navigate = useNavigate(); + const { "home-test-terms-of-use": content } = useContent(); return ( navigate(-1)}> @@ -91,37 +125,7 @@ export default function HomeTestTermsOfUsePage() { {renderParagraphs(content.introduction)} - {content.sections.map((section) => ( -
    -

    - {section.heading} -

    - - {renderParagraphs(section.paragraphs)} - - {section.subsections?.map((subsection, subIndex) => ( -
    - {subsection.heading &&

    {subsection.heading}

    } - - {subsection.paragraphs && renderParagraphs(subsection.paragraphs)} - - {subsection.list && - renderList( - subsection.list, - subsection.ordered, - subsection.indented, - subsection.listStyle, - )} - - {subsection.table && renderTable(subsection.table)} -
    - ))} -
    - ))} + {content.sections.map(renderSection)}
    ); } From 13cdd9b44d4916b2d2c3069894baedd3813b8c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 14:24:15 +0100 Subject: [PATCH 2/9] fix window to globalthis --- ui/src/lib/services/session-storage-service.ts | 8 ++++---- ui/src/routes/HomeTestTermsOfUsePage.tsx | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ui/src/lib/services/session-storage-service.ts b/ui/src/lib/services/session-storage-service.ts index 9e9f477db..ef6af5c8d 100644 --- a/ui/src/lib/services/session-storage-service.ts +++ b/ui/src/lib/services/session-storage-service.ts @@ -1,6 +1,6 @@ class SessionStorageService { private isAvailable(): boolean { - return typeof window !== "undefined" && typeof window.sessionStorage !== "undefined"; + return globalThis.sessionStorage !== undefined; } rehydrate(key: string, fallback: T): T { @@ -8,7 +8,7 @@ class SessionStorageService { return fallback; } - const rawValue = window.sessionStorage.getItem(key); + const rawValue = globalThis.sessionStorage.getItem(key); if (!rawValue) { return fallback; } @@ -26,7 +26,7 @@ class SessionStorageService { return; } - window.sessionStorage.setItem(key, JSON.stringify(value)); + globalThis.sessionStorage.setItem(key, JSON.stringify(value)); } remove(key: string): void { @@ -34,7 +34,7 @@ class SessionStorageService { return; } - window.sessionStorage.removeItem(key); + globalThis.sessionStorage.removeItem(key); } } diff --git a/ui/src/routes/HomeTestTermsOfUsePage.tsx b/ui/src/routes/HomeTestTermsOfUsePage.tsx index 8f4f08421..dcd7f5586 100644 --- a/ui/src/routes/HomeTestTermsOfUsePage.tsx +++ b/ui/src/routes/HomeTestTermsOfUsePage.tsx @@ -82,7 +82,9 @@ const renderTable = (table: { caption?: string; headers: string[]; rows: string[ ))} - {table.rows.map(renderTableRow)} + + {table.rows.map((row, rowIdx) => renderTableRow(row, rowIdx))} + ); From e992729f1c75e5b6eb5f25293a383b769a709983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 14:26:38 +0100 Subject: [PATCH 3/9] fixed the order of function parameters --- ui/src/routes/HomeTestTermsOfUsePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/routes/HomeTestTermsOfUsePage.tsx b/ui/src/routes/HomeTestTermsOfUsePage.tsx index dcd7f5586..c5ad9a154 100644 --- a/ui/src/routes/HomeTestTermsOfUsePage.tsx +++ b/ui/src/routes/HomeTestTermsOfUsePage.tsx @@ -16,7 +16,7 @@ const renderTableCellLines = (cell: string, rowIdx: number, cellIdx: number) =>

    )); -const renderTableCell = (cell: string, cellIdx: number, rowIdx: number) => ( +const renderTableCell = (cell: string, rowIdx: number, cellIdx: number) => ( {renderTableCellLines(cell, rowIdx, cellIdx)} @@ -24,7 +24,7 @@ const renderTableCell = (cell: string, cellIdx: number, rowIdx: number) => ( const renderTableRow = (row: string[], rowIdx: number) => ( - {row.map((cell, cellIdx) => renderTableCell(cell, cellIdx, rowIdx))} + {row.map((cell, cellIdx) => renderTableCell(cell, rowIdx, cellIdx))} ); From a34a7503c9f215542b2acbbe23c3ff3844b354c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 14:31:48 +0100 Subject: [PATCH 4/9] resolve ambiguous spacing issue --- .../CheckYourAnswersPage.tsx | 8 +++---- .../GetSelfTestKitPage.tsx | 23 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/ui/src/routes/get-self-test-kit-for-HIV-journey/CheckYourAnswersPage.tsx b/ui/src/routes/get-self-test-kit-for-HIV-journey/CheckYourAnswersPage.tsx index cc05b1bba..bb007cc93 100644 --- a/ui/src/routes/get-self-test-kit-for-HIV-journey/CheckYourAnswersPage.tsx +++ b/ui/src/routes/get-self-test-kit-for-HIV-journey/CheckYourAnswersPage.tsx @@ -252,7 +252,7 @@ export default function CheckYourAnswersPage() { error={consentError || undefined} > - {content.consent.label.replace("{supplier}", supplierName)}{" "} + {`${content.consent.label.replace("{supplier}", supplierName)} `} { @@ -261,8 +261,8 @@ export default function CheckYourAnswersPage() { }} > {content.consent.termsOfUseText} - {" "} - {content.consent.labelAnd}{" "} + + {` ${content.consent.labelAnd} `} { @@ -272,7 +272,7 @@ export default function CheckYourAnswersPage() { > {content.consent.privacyPolicyText} - . + {"."} diff --git a/ui/src/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage.tsx b/ui/src/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage.tsx index f56f2b87f..ed253f4c9 100644 --- a/ui/src/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage.tsx +++ b/ui/src/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage.tsx @@ -34,8 +34,9 @@ export default function GetSelfTestKitPage() {

    - {content.urgentCard.aeAdvice}{" "} - {commonContent.links.nearestAE.text}. + {`${content.urgentCard.aeAdvice} `} + {commonContent.links.nearestAE.text} + {"."}

    @@ -70,25 +71,27 @@ export default function GetSelfTestKitPage() {

    {content.aboutService.heading}

    - {content.aboutService.text}{" "} - {content.aboutService.termsLink} and{" "} - {content.aboutService.privacyLink}. + {`${content.aboutService.text} `} + {content.aboutService.termsLink} + {` and `} + {content.aboutService.privacyLink} + {"."}

    {content.otherOptions.heading}

    - {content.otherOptions.clinicText}{" "} + {`${content.otherOptions.clinicText} `} {content.otherOptions.clinicLinkText} - {" "} - {content.otherOptions.clinicTextEnd} + + {` ${content.otherOptions.clinicTextEnd}`}

    - {content.otherOptions.sexualHealthText}{" "} + {`${content.otherOptions.sexualHealthText} `} {content.otherOptions.sexualHealthLink.text} - . + {"."}

    From 2c891e6a1748a3a541b12d1d0b9b86801971d1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 14:37:07 +0100 Subject: [PATCH 5/9] added test for session storage service --- .../services/session-storage-service.test.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ui/src/__tests__/lib/services/session-storage-service.test.ts diff --git a/ui/src/__tests__/lib/services/session-storage-service.test.ts b/ui/src/__tests__/lib/services/session-storage-service.test.ts new file mode 100644 index 000000000..7542f20c0 --- /dev/null +++ b/ui/src/__tests__/lib/services/session-storage-service.test.ts @@ -0,0 +1,71 @@ +import sessionStorageService from "@/lib/services/session-storage-service"; + +describe("SessionStorageService", () => { + beforeEach(() => { + window.sessionStorage.clear(); + }); + + describe("rehydrate", () => { + it("returns fallback when sessionStorage is unavailable", () => { + const original = Object.getOwnPropertyDescriptor(globalThis, "sessionStorage"); + Object.defineProperty(globalThis, "sessionStorage", { value: undefined, configurable: true }); + + expect(sessionStorageService.rehydrate("key", "fallback")).toBe("fallback"); + + Object.defineProperty(globalThis, "sessionStorage", original!); + }); + + it("returns fallback when key does not exist", () => { + expect(sessionStorageService.rehydrate("missing-key", 42)).toBe(42); + }); + + it("returns parsed value when key exists with valid JSON", () => { + window.sessionStorage.setItem("my-key", JSON.stringify({ foo: "bar" })); + + expect(sessionStorageService.rehydrate("my-key", null)).toEqual({ foo: "bar" }); + }); + + it("returns fallback and removes key when stored value is invalid JSON", () => { + window.sessionStorage.setItem("bad-key", "not-valid-json{"); + + expect(sessionStorageService.rehydrate("bad-key", "default")).toBe("default"); + expect(window.sessionStorage.getItem("bad-key")).toBeNull(); + }); + }); + + describe("dehydrate", () => { + it("does nothing when sessionStorage is unavailable", () => { + const original = Object.getOwnPropertyDescriptor(globalThis, "sessionStorage"); + Object.defineProperty(globalThis, "sessionStorage", { value: undefined, configurable: true }); + + expect(() => sessionStorageService.dehydrate("key", { x: 1 })).not.toThrow(); + + Object.defineProperty(globalThis, "sessionStorage", original!); + }); + + it("stores value as JSON string", () => { + sessionStorageService.dehydrate("my-key", { a: 1, b: true }); + + expect(window.sessionStorage.getItem("my-key")).toBe(JSON.stringify({ a: 1, b: true })); + }); + }); + + describe("remove", () => { + it("does nothing when sessionStorage is unavailable", () => { + const original = Object.getOwnPropertyDescriptor(globalThis, "sessionStorage"); + Object.defineProperty(globalThis, "sessionStorage", { value: undefined, configurable: true }); + + expect(() => sessionStorageService.remove("key")).not.toThrow(); + + Object.defineProperty(globalThis, "sessionStorage", original!); + }); + + it("removes the key from sessionStorage", () => { + window.sessionStorage.setItem("to-remove", "value"); + + sessionStorageService.remove("to-remove"); + + expect(window.sessionStorage.getItem("to-remove")).toBeNull(); + }); + }); +}); From 9070a99540db897c6f3b05fdfb8bd35eeb272369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 14:39:54 +0100 Subject: [PATCH 6/9] removed empty files and fix lint issues --- .../CannotUseServiceUnder18Page.test.tsx | 9 ++++----- ui/src/app/globals.css | 0 ui/src/app/layout.tsx | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 ui/src/app/globals.css diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/CannotUseServiceUnder18Page.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/CannotUseServiceUnder18Page.test.tsx index 049e0a822..fce5955b2 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/CannotUseServiceUnder18Page.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/CannotUseServiceUnder18Page.test.tsx @@ -1,6 +1,10 @@ import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import React, { ReactNode } from "react"; +import { MemoryRouter } from "react-router-dom"; import { CannotUseServiceUnder18Content, CommonContent } from "@/content"; +import { JourneyStepNames, RoutePath } from "@/lib/models/route-paths"; import CannotUseServiceUnder18Page, { HARD_CODED_CLINIC_DATA, NHS_LINKS, @@ -8,14 +12,9 @@ import CannotUseServiceUnder18Page, { import { CreateOrderProvider, JourneyNavigationContext, - JourneyNavigationProvider, OrderAnswers, useCreateOrderContext, } from "@/state"; -import { JourneyStepNames, RoutePath } from "@/lib/models/route-paths"; -import React, { ReactNode } from "react"; -import { fireEvent, render, screen } from "@testing-library/react"; -import { MemoryRouter } from "react-router-dom"; const mockedContent: CannotUseServiceUnder18Content = { title: "some-mocked-title", diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index 6244d3cf9..0737baba6 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -1,4 +1,3 @@ -import "./globals.css"; import "nhsuk-frontend/dist/nhsuk/nhsuk-frontend.css"; import type { Metadata } from "next"; From a87ead75e2a0e422282f17127538538a1915abdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 14:46:52 +0100 Subject: [PATCH 7/9] fix test for edge case --- .../services/session-storage-service.test.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/ui/src/__tests__/lib/services/session-storage-service.test.ts b/ui/src/__tests__/lib/services/session-storage-service.test.ts index 7542f20c0..94e583e01 100644 --- a/ui/src/__tests__/lib/services/session-storage-service.test.ts +++ b/ui/src/__tests__/lib/services/session-storage-service.test.ts @@ -9,10 +9,11 @@ describe("SessionStorageService", () => { it("returns fallback when sessionStorage is unavailable", () => { const original = Object.getOwnPropertyDescriptor(globalThis, "sessionStorage"); Object.defineProperty(globalThis, "sessionStorage", { value: undefined, configurable: true }); - - expect(sessionStorageService.rehydrate("key", "fallback")).toBe("fallback"); - - Object.defineProperty(globalThis, "sessionStorage", original!); + try { + expect(sessionStorageService.rehydrate("key", "fallback")).toBe("fallback"); + } finally { + Object.defineProperty(globalThis, "sessionStorage", original!); + } }); it("returns fallback when key does not exist", () => { @@ -37,10 +38,11 @@ describe("SessionStorageService", () => { it("does nothing when sessionStorage is unavailable", () => { const original = Object.getOwnPropertyDescriptor(globalThis, "sessionStorage"); Object.defineProperty(globalThis, "sessionStorage", { value: undefined, configurable: true }); - - expect(() => sessionStorageService.dehydrate("key", { x: 1 })).not.toThrow(); - - Object.defineProperty(globalThis, "sessionStorage", original!); + try { + expect(() => sessionStorageService.dehydrate("key", { x: 1 })).not.toThrow(); + } finally { + Object.defineProperty(globalThis, "sessionStorage", original!); + } }); it("stores value as JSON string", () => { @@ -54,10 +56,11 @@ describe("SessionStorageService", () => { it("does nothing when sessionStorage is unavailable", () => { const original = Object.getOwnPropertyDescriptor(globalThis, "sessionStorage"); Object.defineProperty(globalThis, "sessionStorage", { value: undefined, configurable: true }); - - expect(() => sessionStorageService.remove("key")).not.toThrow(); - - Object.defineProperty(globalThis, "sessionStorage", original!); + try { + expect(() => sessionStorageService.remove("key")).not.toThrow(); + } finally { + Object.defineProperty(globalThis, "sessionStorage", original!); + } }); it("removes the key from sessionStorage", () => { From bfdb76ed08f3cc6d7961705c2bb0eb63ec0510ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 15:34:31 +0100 Subject: [PATCH 8/9] fixes for suggestions from sonarqube --- ui/jest.setup.ts | 4 ++-- ui/src/routes/HomeTestPrivacyPolicyPage.tsx | 12 +++++++----- ui/src/routes/HomeTestTermsOfUsePage.tsx | 2 +- .../EnterAddressManuallyPage.tsx | 11 ++++++----- .../SelectDeliveryAddressPage.tsx | 17 +++++++++-------- ui/src/state/NavigationContext.tsx | 4 ++-- ui/src/state/PostcodeLookupContext.tsx | 11 ++++++----- 7 files changed, 33 insertions(+), 28 deletions(-) diff --git a/ui/jest.setup.ts b/ui/jest.setup.ts index d53820468..fac89734e 100644 --- a/ui/jest.setup.ts +++ b/ui/jest.setup.ts @@ -29,13 +29,13 @@ const createStorageMock = (): Storage => { const sessionStorageMock = createStorageMock(); -Object.defineProperty(window, "sessionStorage", { +Object.defineProperty(globalThis, "sessionStorage", { value: sessionStorageMock, configurable: true, }); beforeEach(() => { - window.sessionStorage.clear(); + globalThis.sessionStorage.clear(); }); globalThis.TextEncoder = TextEncoder; diff --git a/ui/src/routes/HomeTestPrivacyPolicyPage.tsx b/ui/src/routes/HomeTestPrivacyPolicyPage.tsx index 71381ab40..8c2ac8711 100644 --- a/ui/src/routes/HomeTestPrivacyPolicyPage.tsx +++ b/ui/src/routes/HomeTestPrivacyPolicyPage.tsx @@ -1,17 +1,19 @@ "use client"; -import PageLayout from "@/layouts/PageLayout"; -import { useContent } from "@/hooks"; -import { useNavigate } from "react-router-dom"; -import { renderTextWithLinks, cleanListItems, getListClass } from "@/utils/renderTextWithLinks"; import "@/styles/lists.css"; +import { useNavigate } from "react-router-dom"; + +import { useContent } from "@/hooks"; +import PageLayout from "@/layouts/PageLayout"; +import { cleanListItems, getListClass, renderTextWithLinks } from "@/utils/renderTextWithLinks"; + export default function HomeTestPrivacyPolicyPage() { const navigate = useNavigate(); const { "home-test-privacy-policy": content } = useContent(); const renderHeading = (text: string) => { - const numberMatch = text.match(/^(\d+\.\s+)/); + const numberMatch = /^(\d+\.\s+)/.exec(text); if (numberMatch) { return ( <> diff --git a/ui/src/routes/HomeTestTermsOfUsePage.tsx b/ui/src/routes/HomeTestTermsOfUsePage.tsx index c5ad9a154..bddb5ca33 100644 --- a/ui/src/routes/HomeTestTermsOfUsePage.tsx +++ b/ui/src/routes/HomeTestTermsOfUsePage.tsx @@ -33,7 +33,7 @@ const renderTableRow = (row: string[], rowIdx: number) => ( */ const renderParagraphs = (paragraphs: string[]) => paragraphs.map((paragraph, index) => { - const numberMatch = paragraph.match(/^(\d+\.\d+\.?\s+)/); + const numberMatch = /^(\d+\.\d+\.?\s+)/.exec(paragraph); if (numberMatch) { const number = numberMatch[1]; const rest = paragraph.slice(number.length); diff --git a/ui/src/routes/get-self-test-kit-for-HIV-journey/EnterAddressManuallyPage.tsx b/ui/src/routes/get-self-test-kit-for-HIV-journey/EnterAddressManuallyPage.tsx index a91d0c556..7062effbb 100644 --- a/ui/src/routes/get-self-test-kit-for-HIV-journey/EnterAddressManuallyPage.tsx +++ b/ui/src/routes/get-self-test-kit-for-HIV-journey/EnterAddressManuallyPage.tsx @@ -1,14 +1,15 @@ "use client"; import { Button, ErrorSummary, TextInput } from "nhsuk-react-components"; -import { useAuth, useCreateOrderContext, useJourneyNavigationContext } from "@/state"; +import { useState } from "react"; + +import type { ValidationMessages } from "@/content"; +import { useContent } from "@/hooks"; import FormPageLayout from "@/layouts/FormPageLayout"; import { JourneyStepNames } from "@/lib/models/route-paths"; -import type { ValidationMessages } from "@/content"; import laLookupService from "@/lib/services/la-lookup-service"; -import { useContent } from "@/hooks"; -import { useState } from "react"; import { isUnder18 } from "@/lib/utils/is-under-18"; +import { useAuth, useCreateOrderContext, useJourneyNavigationContext } from "@/state"; const POSTCODE_REGEX = /^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$/i; const MAX_POSTCODE_LENGTH = 8; @@ -211,7 +212,7 @@ export default function EnterAddressManuallyPage() { try { const postcode = postcodeValidation.value; const laResponse = await laLookupService.getByPostcode(postcode); - if (!laResponse || !laResponse.suppliers || laResponse.suppliers.length === 0) { + if (!laResponse?.suppliers?.length) { updateOrderAnswers({ postcodeSearch: postcode }); goToStep(JourneyStepNames.KitNotAvailableInArea); return; diff --git a/ui/src/routes/get-self-test-kit-for-HIV-journey/SelectDeliveryAddressPage.tsx b/ui/src/routes/get-self-test-kit-for-HIV-journey/SelectDeliveryAddressPage.tsx index d166dc571..3792574cf 100644 --- a/ui/src/routes/get-self-test-kit-for-HIV-journey/SelectDeliveryAddressPage.tsx +++ b/ui/src/routes/get-self-test-kit-for-HIV-journey/SelectDeliveryAddressPage.tsx @@ -1,5 +1,13 @@ "use client"; +import { Button, ErrorSummary, Radios } from "nhsuk-react-components"; +import { useState } from "react"; + +import { useAsyncErrorHandler, useContent } from "@/hooks"; +import FormPageLayout from "@/layouts/FormPageLayout"; +import { JourneyStepNames } from "@/lib/models/route-paths"; +import laLookupService from "@/lib/services/la-lookup-service"; +import { isUnder18 } from "@/lib/utils/is-under-18"; import { AddressResult, useAuth, @@ -7,13 +15,6 @@ import { useJourneyNavigationContext, usePostcodeLookup, } from "@/state"; -import FormPageLayout from "@/layouts/FormPageLayout"; -import { useContent, useAsyncErrorHandler } from "@/hooks"; -import { Radios, Button, ErrorSummary } from "nhsuk-react-components"; -import { JourneyStepNames } from "@/lib/models/route-paths"; -import laLookupService from "@/lib/services/la-lookup-service"; -import { useState } from "react"; -import { isUnder18 } from "@/lib/utils/is-under-18"; export default function SelectDeliveryAddressPage() { const { goToStep, goBack, stepHistory, returnToStep, setReturnToStep } = @@ -47,7 +48,7 @@ export default function SelectDeliveryAddressPage() { const postcode = selected.postcode; const laResponse = await laLookupService.getByPostcode(postcode); - if (!laResponse || !laResponse.suppliers || laResponse.suppliers.length === 0) { + if (!laResponse?.suppliers?.length) { updateOrderAnswers({ postcodeSearch: postcode }); goToStep(JourneyStepNames.KitNotAvailableInArea); return; diff --git a/ui/src/state/NavigationContext.tsx b/ui/src/state/NavigationContext.tsx index 41a64aff4..46d7d4970 100644 --- a/ui/src/state/NavigationContext.tsx +++ b/ui/src/state/NavigationContext.tsx @@ -73,7 +73,7 @@ export function JourneyNavigationProvider({ children }: Readonly<{ children: Rea const stepHistory = persistedState.stepHistory.length > 0 ? persistedState.stepHistory : [currentStep]; - const lastStep = stepHistory[stepHistory.length - 1]; + const lastStep = stepHistory.at(-1); return { stepHistory: lastStep === currentStep ? stepHistory : [...stepHistory, currentStep], @@ -82,7 +82,7 @@ export function JourneyNavigationProvider({ children }: Readonly<{ children: Rea }); const stepHistory = useMemo(() => { - const lastStep = navigation.stepHistory[navigation.stepHistory.length - 1]; + const lastStep = navigation.stepHistory.at(-1); return lastStep === currentStep ? navigation.stepHistory diff --git a/ui/src/state/PostcodeLookupContext.tsx b/ui/src/state/PostcodeLookupContext.tsx index 82d8cc1e2..dd098e846 100644 --- a/ui/src/state/PostcodeLookupContext.tsx +++ b/ui/src/state/PostcodeLookupContext.tsx @@ -6,9 +6,12 @@ import React, { useEffect, useState, } from "react"; + import sessionService from "@/lib/services/session-service"; import { backendUrl } from "@/settings"; +export type LookupResultsStatus = "idle" | "found" | "not_found" | "error"; + export interface AddressResult { id: string; line1: string; @@ -25,7 +28,7 @@ export interface PostcodeLookupContextType { addresses: AddressResult[]; selectedAddress: AddressResult | null; isLoading: boolean; - lookupResultsStatus: "idle" | "found" | "not_found" | "error"; + lookupResultsStatus: LookupResultsStatus; error: string | null; lookupPostcode: (postcode: string) => Promise; setSelectedAddress: (address: AddressResult | null) => void; @@ -44,7 +47,7 @@ interface PersistedPostcodeLookupState { postcode: string; addresses: AddressResult[]; selectedAddress: AddressResult | null; - lookupResultsStatus: "idle" | "found" | "not_found" | "error"; + lookupResultsStatus: LookupResultsStatus; error: string | null; } @@ -61,9 +64,7 @@ export const PostcodeLookupProvider: React.FC = ({ const [addresses, setAddresses] = useState([]); const [selectedAddress, setSelectedAddress] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [lookupResultsStatus, setLookupResultsStatus] = useState< - "idle" | "found" | "not_found" | "error" - >("idle"); + const [lookupResultsStatus, setLookupResultsStatus] = useState("idle"); const [error, setError] = useState(null); const [hasHydrated, setHasHydrated] = useState(false); From 373a8cc7860a6125a2f499235e877260e8684c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Rejterada?= Date: Thu, 26 Mar 2026 15:44:47 +0100 Subject: [PATCH 9/9] use globalThis in tests --- .../lib/services/session-service.test.ts | 20 +++++++++---------- .../services/session-storage-service.test.ts | 14 ++++++------- ui/src/__tests__/state/AuthContext.test.tsx | 10 +++++----- ui/src/__tests__/state/OrderContext.test.tsx | 16 ++++++++------- .../state/PostcodeLookupContext.test.tsx | 18 ++++++++++------- 5 files changed, 42 insertions(+), 36 deletions(-) diff --git a/ui/src/__tests__/lib/services/session-service.test.ts b/ui/src/__tests__/lib/services/session-service.test.ts index ffbd3af09..d38b29f73 100644 --- a/ui/src/__tests__/lib/services/session-service.test.ts +++ b/ui/src/__tests__/lib/services/session-service.test.ts @@ -2,7 +2,7 @@ import sessionService, { SESSION_STORAGE_KEYS } from "@/lib/services/session-ser describe("SessionService", () => { beforeEach(() => { - window.sessionStorage.clear(); + globalThis.sessionStorage.clear(); }); describe("auth user", () => { @@ -18,21 +18,21 @@ describe("SessionService", () => { sessionService.dehydrateAuthUser(user); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.authUser)).toBe( + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.authUser)).toBe( JSON.stringify(user), ); expect(sessionService.rehydrateAuthUser()).toEqual(user); }); it("clears auth user when null is provided", () => { - window.sessionStorage.setItem( + globalThis.sessionStorage.setItem( SESSION_STORAGE_KEYS.authUser, JSON.stringify({ sub: "existing" }), ); sessionService.dehydrateAuthUser(null); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.authUser)).toBeNull(); + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.authUser)).toBeNull(); }); }); @@ -54,13 +54,13 @@ describe("SessionService", () => { sessionService.dehydrateJourneyNavigation(navigation); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.journeyNavigation)).toBe( + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.journeyNavigation)).toBe( JSON.stringify(navigation), ); sessionService.clearJourneyNavigation(); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.journeyNavigation)).toBeNull(); + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.journeyNavigation)).toBeNull(); }); }); @@ -79,13 +79,13 @@ describe("SessionService", () => { sessionService.dehydrateCreateOrderAnswers(answers); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).toBe( + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).toBe( JSON.stringify(answers), ); sessionService.clearCreateOrderAnswers(); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).toBeNull(); + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).toBeNull(); }); }); @@ -118,13 +118,13 @@ describe("SessionService", () => { sessionService.dehydratePostcodeLookup(postcodeLookup); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup)).toBe( + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup)).toBe( JSON.stringify(postcodeLookup), ); sessionService.clearPostcodeLookup(); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup)).toBeNull(); + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup)).toBeNull(); }); }); }); diff --git a/ui/src/__tests__/lib/services/session-storage-service.test.ts b/ui/src/__tests__/lib/services/session-storage-service.test.ts index 94e583e01..60bd9f8b3 100644 --- a/ui/src/__tests__/lib/services/session-storage-service.test.ts +++ b/ui/src/__tests__/lib/services/session-storage-service.test.ts @@ -2,7 +2,7 @@ import sessionStorageService from "@/lib/services/session-storage-service"; describe("SessionStorageService", () => { beforeEach(() => { - window.sessionStorage.clear(); + globalThis.sessionStorage.clear(); }); describe("rehydrate", () => { @@ -21,16 +21,16 @@ describe("SessionStorageService", () => { }); it("returns parsed value when key exists with valid JSON", () => { - window.sessionStorage.setItem("my-key", JSON.stringify({ foo: "bar" })); + globalThis.sessionStorage.setItem("my-key", JSON.stringify({ foo: "bar" })); expect(sessionStorageService.rehydrate("my-key", null)).toEqual({ foo: "bar" }); }); it("returns fallback and removes key when stored value is invalid JSON", () => { - window.sessionStorage.setItem("bad-key", "not-valid-json{"); + globalThis.sessionStorage.setItem("bad-key", "not-valid-json{"); expect(sessionStorageService.rehydrate("bad-key", "default")).toBe("default"); - expect(window.sessionStorage.getItem("bad-key")).toBeNull(); + expect(globalThis.sessionStorage.getItem("bad-key")).toBeNull(); }); }); @@ -48,7 +48,7 @@ describe("SessionStorageService", () => { it("stores value as JSON string", () => { sessionStorageService.dehydrate("my-key", { a: 1, b: true }); - expect(window.sessionStorage.getItem("my-key")).toBe(JSON.stringify({ a: 1, b: true })); + expect(globalThis.sessionStorage.getItem("my-key")).toBe(JSON.stringify({ a: 1, b: true })); }); }); @@ -64,11 +64,11 @@ describe("SessionStorageService", () => { }); it("removes the key from sessionStorage", () => { - window.sessionStorage.setItem("to-remove", "value"); + globalThis.sessionStorage.setItem("to-remove", "value"); sessionStorageService.remove("to-remove"); - expect(window.sessionStorage.getItem("to-remove")).toBeNull(); + expect(globalThis.sessionStorage.getItem("to-remove")).toBeNull(); }); }); }); diff --git a/ui/src/__tests__/state/AuthContext.test.tsx b/ui/src/__tests__/state/AuthContext.test.tsx index 071de1443..2fc22b09e 100644 --- a/ui/src/__tests__/state/AuthContext.test.tsx +++ b/ui/src/__tests__/state/AuthContext.test.tsx @@ -1,7 +1,7 @@ import "@testing-library/jest-dom"; +import { act, render, renderHook, screen } from "@testing-library/react"; import { AuthProvider, AuthUser, useAuth } from "@/state"; -import { act, render, renderHook, screen } from "@testing-library/react"; const AUTH_STORAGE_KEY = "hometest:auth:user"; @@ -18,7 +18,7 @@ describe("AuthContext", () => { }; beforeEach(() => { - window.sessionStorage.clear(); + globalThis.sessionStorage.clear(); }); describe("AuthProvider", () => { @@ -42,7 +42,7 @@ describe("AuthContext", () => { }); it("rehydrates user state from session storage", () => { - window.sessionStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(mockUser)); + globalThis.sessionStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(mockUser)); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, @@ -94,7 +94,7 @@ describe("AuthContext", () => { expect(result.current.user).toEqual(mockUser); expect(result.current.user?.nhsNumber).toBe("9876543210"); expect(result.current.user?.birthdate).toBe("1985-05-15"); - expect(window.sessionStorage.getItem(AUTH_STORAGE_KEY)).toBe(JSON.stringify(mockUser)); + expect(globalThis.sessionStorage.getItem(AUTH_STORAGE_KEY)).toBe(JSON.stringify(mockUser)); }); it("clears user state when setUser is called with null", () => { @@ -115,7 +115,7 @@ describe("AuthContext", () => { }); expect(result.current.user).toBeNull(); - expect(window.sessionStorage.getItem(AUTH_STORAGE_KEY)).toBeNull(); + expect(globalThis.sessionStorage.getItem(AUTH_STORAGE_KEY)).toBeNull(); }); it("updates user correctly when setUser is called multiple times", () => { diff --git a/ui/src/__tests__/state/OrderContext.test.tsx b/ui/src/__tests__/state/OrderContext.test.tsx index b40b89034..47cb5492a 100644 --- a/ui/src/__tests__/state/OrderContext.test.tsx +++ b/ui/src/__tests__/state/OrderContext.test.tsx @@ -1,12 +1,12 @@ import "@testing-library/jest-dom"; +import { act, renderHook } from "@testing-library/react"; -import { CreateOrderProvider, useCreateOrderContext } from "@/state/OrderContext"; import { SESSION_STORAGE_KEYS } from "@/lib/services/session-service"; -import { act, renderHook } from "@testing-library/react"; +import { CreateOrderProvider, useCreateOrderContext } from "@/state/OrderContext"; describe("OrderContext", () => { beforeEach(() => { - window.sessionStorage.clear(); + globalThis.sessionStorage.clear(); }); describe("CreateOrderProvider", () => { @@ -24,7 +24,7 @@ describe("OrderContext", () => { mobileNumber: "07700900123", }; - window.sessionStorage.setItem( + globalThis.sessionStorage.setItem( SESSION_STORAGE_KEYS.createOrderAnswers, JSON.stringify(persistedAnswers), ); @@ -52,7 +52,7 @@ describe("OrderContext", () => { postcodeSearch: "SW1A 1AA", mobileNumber: "07700900123", }); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).toBe( + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).toBe( JSON.stringify({ postcodeSearch: "SW1A 1AA", mobileNumber: "07700900123", @@ -69,14 +69,16 @@ describe("OrderContext", () => { result.current.updateOrderAnswers({ postcodeSearch: "SW1A 1AA" }); }); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).not.toBeNull(); + expect( + globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers), + ).not.toBeNull(); act(() => { result.current.reset(); }); expect(result.current.orderAnswers).toEqual({}); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).toBeNull(); + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.createOrderAnswers)).toBeNull(); }); }); diff --git a/ui/src/__tests__/state/PostcodeLookupContext.test.tsx b/ui/src/__tests__/state/PostcodeLookupContext.test.tsx index f8bb94723..4b17aca2c 100644 --- a/ui/src/__tests__/state/PostcodeLookupContext.test.tsx +++ b/ui/src/__tests__/state/PostcodeLookupContext.test.tsx @@ -1,8 +1,8 @@ import "@testing-library/jest-dom"; +import { act, renderHook, waitFor } from "@testing-library/react"; -import { PostcodeLookupProvider, usePostcodeLookup } from "@/state/PostcodeLookupContext"; import { SESSION_STORAGE_KEYS } from "@/lib/services/session-service"; -import { act, renderHook, waitFor } from "@testing-library/react"; +import { PostcodeLookupProvider, usePostcodeLookup } from "@/state/PostcodeLookupContext"; jest.mock("@/settings", () => ({ backendUrl: "http://mock-backend" })); @@ -11,7 +11,7 @@ globalThis.fetch = mockFetch as typeof fetch; describe("PostcodeLookupContext", () => { beforeEach(() => { - window.sessionStorage.clear(); + globalThis.sessionStorage.clear(); jest.clearAllMocks(); }); @@ -51,7 +51,7 @@ describe("PostcodeLookupContext", () => { error: null, }; - window.sessionStorage.setItem( + globalThis.sessionStorage.setItem( SESSION_STORAGE_KEYS.postcodeLookup, JSON.stringify(persistedState), ); @@ -98,7 +98,9 @@ describe("PostcodeLookupContext", () => { expect(result.current.error).toBeNull(); await waitFor(() => { - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup)).not.toBeNull(); + expect( + globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup), + ).not.toBeNull(); }); }); @@ -164,7 +166,9 @@ describe("PostcodeLookupContext", () => { }); await waitFor(() => { - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup)).not.toBeNull(); + expect( + globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup), + ).not.toBeNull(); }); act(() => { @@ -176,7 +180,7 @@ describe("PostcodeLookupContext", () => { expect(result.current.selectedAddress).toBeNull(); expect(result.current.lookupResultsStatus).toBe("idle"); expect(result.current.error).toBeNull(); - expect(window.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup)).toBeNull(); + expect(globalThis.sessionStorage.getItem(SESSION_STORAGE_KEYS.postcodeLookup)).toBeNull(); }); });