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/__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(); }); }); 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);