diff --git a/ui/content/ContentValidator.ts b/ui/content/ContentValidator.ts index 0c8e4ab58..b38bf2ce4 100644 --- a/ui/content/ContentValidator.ts +++ b/ui/content/ContentValidator.ts @@ -40,6 +40,8 @@ const REQUIRED_PAGE_KEYS: (keyof MainPagesContent)[] = [ "how-comfortable-pricking-finger", "cannot-use-service-under-18", "enter-mobile-phone-number", + "check-your-answers", + "confirm-mobile-phone-number", "service-error", "order-tracking", "test-results", @@ -48,6 +50,22 @@ const REQUIRED_PAGE_KEYS: (keyof MainPagesContent)[] = [ "suppliers-privacy-policy", ]; +// order-submitted is required to exist in the content but has no top-level title field, +// so it is kept separate from REQUIRED_PAGE_KEYS to avoid triggering the title assertion. +const REQUIRED_PAGE_PRESENCE_KEYS: ReadonlyArray = [ + ...REQUIRED_PAGE_KEYS, + "order-submitted", +]; + +// All main pages that declare a pageTitle field in their schema interface. +// Supplier pages are excluded because they derive their title at render time via formatPageTitle. +const REQUIRED_PAGE_TITLE_KEYS: ReadonlyArray = [ + ...REQUIRED_PAGE_KEYS.filter( + (k) => k !== "suppliers-terms-conditions" && k !== "suppliers-privacy-policy", + ), + "order-submitted", +]; + const isObject = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); @@ -86,7 +104,7 @@ const validatePagesContent = (content: unknown, errors: string[]): content is Ma return false; } - for (const key of REQUIRED_PAGE_KEYS) { + for (const key of REQUIRED_PAGE_PRESENCE_KEYS) { if (!(key in content)) { errors.push(`pages is missing required key: ${key}`); } else if (!isObject(content[key])) { @@ -101,6 +119,13 @@ const validatePagesContent = (content: unknown, errors: string[]): content is Ma } } + for (const key of REQUIRED_PAGE_TITLE_KEYS) { + const page = content[key]; + if (isObject(page) && !isNonEmptyString(page.pageTitle)) { + errors.push(`pages.${key}.pageTitle must be a non-empty string`); + } + } + return errors.length === 0; }; @@ -152,6 +177,9 @@ const assertValidLegalPageContent = (label: string, content: unknown): void => { if (!isObject(content)) { throw new Error(`${label} content must be an object`); } + if (!isNonEmptyString(content["pageTitle"])) { + errors.push("pageTitle must be a non-empty string"); + } if (!isNonEmptyString(content["title"])) { errors.push("title must be a non-empty string"); } diff --git a/ui/content/content.json b/ui/content/content.json index 4bd500273..d259a45e9 100644 --- a/ui/content/content.json +++ b/ui/content/content.json @@ -125,7 +125,7 @@ }, "pages": { "before-you-start": { - "pageTitle": "Before you order a free HIV self-test kit - HomeTest - NHS", + "pageTitle": "Before you order a free HIV self-test kit – HIV Home Test Service – NHS", "title": "Before you order a free HIV self-test kit", "urgentCard": { "heading": "Go to a sexual health clinic if:", @@ -141,7 +141,7 @@ "continueButton": "Continue to order a kit" }, "get-self-test-kit-for-HIV": { - "pageTitle": "Order a free HIV self-test kit - HomeTest - NHS", + "pageTitle": "Order a free HIV self-test kit – HIV Home Test Service – NHS", "title": "Order a free HIV self-test kit", "eligibility": { "intro": "You can use this service if:", @@ -200,15 +200,18 @@ } }, "kit-not-available-in-area": { + "pageTitle": "Free HIV self-test kits are not available in your area using this service – HIV Home Test Service – NHS", "title": "Free HIV self-test kits are not available in your area using this service", "description": "There are other options to get an HIV test.", "moreOptionsHeading": "More options and information" }, "go-to-clinic": { + "pageTitle": "Contact your nearest sexual health clinic – HIV Home Test Service – NHS", "title": "Contact your nearest sexual health clinic", "moreOptionsHeading": "More options and information" }, "enter-delivery-address": { + "pageTitle": "Enter your delivery address and we'll check if the kit's available – HIV Home Test Service – NHS", "title": "Enter your delivery address and we'll check if the kit's available", "form": { "postcodeLabel": "Postcode", @@ -218,6 +221,7 @@ } }, "enter-address-manually": { + "pageTitle": "Enter your delivery address manually and we'll check if the kit's available – HIV Home Test Service – NHS", "title": "Enter your delivery address manually and we'll check if the kit's available", "form": { "addressLine1Label": "Address line 1", @@ -229,17 +233,20 @@ } }, "no-address-found": { + "pageTitle": "No address found – HIV Home Test Service – NHS", "title": "No address found", "notFoundMessage": "We could not find an address that matches", "tryNewSearchLink": "Try a new search" }, "select-delivery-address": { + "pageTitle": "Select your delivery address – HIV Home Test Service – NHS", "title": "found", "postcodeLabel": "Postcode:", "editPostcodeLink": "Edit postcode", "formLabel": "Select your delivery address" }, "how-comfortable-pricking-finger": { + "pageTitle": "This is what you'll need to do to give a blood sample – HIV Home Test Service – NHS", "title": "This is what you'll need to do to give a blood sample", "instructions": "To give a sample of blood, you'll need to:", "steps": { @@ -262,6 +269,7 @@ } }, "cannot-use-service-under-18": { + "pageTitle": "You cannot use this service as you are under 18 – HIV Home Test Service – NHS", "title": "You cannot use this service as you are under 18", "intro": "To get tested for HIV, go to your nearest sexual health clinic, which is:", "phoneLabel": "Phone:", @@ -274,6 +282,7 @@ "learnMoreLinkHref": "https://www.nhs.uk/conditions/hiv-and-aids/" }, "enter-mobile-phone-number": { + "pageTitle": "What's your mobile phone number? – HIV Home Test Service – NHS", "title": "What's your mobile phone number?", "hint": "{supplier} will send updates to this number. It must be your own number.", "form": { @@ -282,7 +291,8 @@ } }, "confirm-mobile-phone-number": { - "title": "What's your mobile phone number?", + "pageTitle": "Confirm your mobile phone number – HIV Home Test Service – NHS", + "title": "Confirm your mobile phone number", "hint": "{supplier} will send updates to this number. It must be your own number.", "form": { "alternativeLabel": "Use another mobile phone number", @@ -292,10 +302,12 @@ "radioLegend": "Confirm your mobile phone number" }, "service-error": { + "pageTitle": "Sorry, there is a problem with the service – HIV Home Test Service – NHS", "title": "Sorry, there is a problem with the service", "message": "Try again later." }, "order-tracking": { + "pageTitle": "Track your order – HIV Home Test Service – NHS", "title": "Track your order", "error": { "title": "There is a problem", @@ -304,6 +316,7 @@ } }, "test-results": { + "pageTitle": "HIV self-test result – HIV Home Test Service – NHS", "title": "Test results", "error": { "title": "There is a problem", @@ -337,6 +350,7 @@ } }, "blood-sample-guide": { + "pageTitle": "Blood sample step-by-step guide – HIV Home Test Service – NHS", "title": "Blood sample step-by-step guide", "whatsInKit": { "summary": "What's in the kit", @@ -509,6 +523,7 @@ } }, "check-your-answers": { + "pageTitle": "Check your answers before submitting your order – HIV Home Test Service – NHS", "title": "Check your answers before submitting your order", "updateMessage": "We'll update you about your HIV test on the account you use for your NHS login.", "deliveryMessage": "The kit will arrive within 5 working days.", @@ -529,6 +544,7 @@ "submitButton": "Submit order" }, "order-submitted": { + "pageTitle": "Order submitted – HIV Home Test Service – NHS", "panel": { "title": "Order submitted", "referenceNumberPrefix": "Your reference number" diff --git a/ui/content/hometest-privacy-policy.json b/ui/content/hometest-privacy-policy.json index e1f879d2a..4e762636a 100644 --- a/ui/content/hometest-privacy-policy.json +++ b/ui/content/hometest-privacy-policy.json @@ -1,4 +1,5 @@ { + "pageTitle": "Home test privacy policy – HIV Home Test Service – NHS", "title": "Hometest Privacy Policy - Draft v1.0 Jan 2026", "introduction": [ "**[Hometest]** is operated by NHS England [and commissioned by the Department of Health and Social Care (DHSC)].", @@ -25,9 +26,7 @@ ] }, { - "paragraphs": [ - "Further details are set out in the Hometest Terms of Use [LINK]." - ] + "paragraphs": ["Further details are set out in the Hometest Terms of Use [LINK]."] } ] }, @@ -115,9 +114,7 @@ { "heading": "Health Data", "inlineHeading": true, - "paragraphs": [ - "Special category data may also be collected, including test results." - ] + "paragraphs": ["Special category data may also be collected, including test results."] }, { "heading": "Technical Data", @@ -129,9 +126,7 @@ { "heading": "Preference Data", "inlineHeading": true, - "paragraphs": [ - "This includes your preferences on receiving communications from us." - ] + "paragraphs": ["This includes your preferences on receiving communications from us."] }, { "heading": "Usage Data", diff --git a/ui/content/hometest-terms-of-use.json b/ui/content/hometest-terms-of-use.json index b0af9a370..19372becc 100644 --- a/ui/content/hometest-terms-of-use.json +++ b/ui/content/hometest-terms-of-use.json @@ -1,4 +1,5 @@ { + "pageTitle": "Home test terms of use – HIV Home Test Service – NHS", "title": "Hometest Terms of Use - Draft V1 January 2026", "introduction": [ "[Hometest] is operated by NHS England [and commissioned by the Department of Health and Social Care (DHSC)]." @@ -136,11 +137,7 @@ { "table": { "caption": "Name of home testing services with functionality and further information", - "headers": [ - "Service", - "Functionality", - "For further information, see:" - ], + "headers": ["Service", "Functionality", "For further information, see:"], "rows": [ [ "Testing Services\n(Hometest App allows you to access the service, but the service is provided outside the Hometest App)", @@ -245,9 +242,7 @@ { "id": "prohibited-uses", "heading": "10. Prohibited uses", - "paragraphs": [ - "10.1. You may not use the Hometest App:" - ], + "paragraphs": ["10.1. You may not use the Hometest App:"], "subsections": [ { "indented": true, diff --git a/ui/content/schema.ts b/ui/content/schema.ts index b75da1b8c..929a27fd5 100644 --- a/ui/content/schema.ts +++ b/ui/content/schema.ts @@ -225,6 +225,7 @@ export interface StartPageContent { } export interface EnterDeliveryAddressContent { + pageTitle: string; title: string; form: { postcodeLabel: string; @@ -235,6 +236,7 @@ export interface EnterDeliveryAddressContent { } export interface EnterAddressManuallyContent { + pageTitle: string; title: string; form: { addressLine1Label: string; @@ -247,12 +249,14 @@ export interface EnterAddressManuallyContent { } export interface NoAddressFoundContent { + pageTitle: string; title: string; notFoundMessage: string; tryNewSearchLink: string; } export interface SelectDeliveryAddressContent { + pageTitle: string; title: string; postcodeLabel: string; editPostcodeLink: string; @@ -260,6 +264,7 @@ export interface SelectDeliveryAddressContent { } export interface HowComfortablePrickingFingerContent { + pageTitle: string; title: string; instructions: string; steps: { @@ -283,6 +288,7 @@ export interface HowComfortablePrickingFingerContent { } export interface CannotUseServiceUnder18Content { + pageTitle: string; title: string; intro: string; phoneLabel: string; @@ -296,6 +302,7 @@ export interface CannotUseServiceUnder18Content { } export interface EnterMobilePhoneNumberContent { + pageTitle: string; title: string; hint: string; form: { @@ -305,6 +312,7 @@ export interface EnterMobilePhoneNumberContent { } export interface ConfirmMobilePhoneNumberContent { + pageTitle: string; title: string; hint: string; form: { @@ -316,11 +324,13 @@ export interface ConfirmMobilePhoneNumberContent { } export interface ServiceErrorContent { + pageTitle: string; title: string; message: string; } export interface OrderTrackingContent { + pageTitle: string; error: { title: string; orderNotFound: string; @@ -329,6 +339,7 @@ export interface OrderTrackingContent { } export interface TestResultsContent { + pageTitle: string; title: string; error: { title: string; @@ -385,18 +396,21 @@ export interface PrivacyPolicySection { } export interface HomeTestPrivacyPolicyContent { + pageTitle: string; title: string; introduction: string[]; sections: PrivacyPolicySection[]; } export interface HomeTestTermsOfUseContent { + pageTitle: string; title: string; introduction: string[]; sections: PrivacyPolicySection[]; } export interface BloodSampleGuideContent { + pageTitle: string; title: string; whatsInKit: { summary: string; @@ -423,6 +437,7 @@ export interface BloodSampleGuideContent { } export interface CheckYourAnswersContent { + pageTitle: string; title: string; updateMessage: string; deliveryMessage: string; @@ -444,6 +459,7 @@ export interface CheckYourAnswersContent { } export interface OrderSubmittedContent { + pageTitle: string; panel: { title: string; referenceNumberPrefix: string; @@ -459,6 +475,7 @@ export interface OrderSubmittedContent { }; } export interface KitNotAvailableInAreaContent { + pageTitle: string; title: string; description: string; moreOptionsHeading: string; @@ -489,6 +506,7 @@ export interface SuppliersLegalDocumentsContent = jest.fn< @@ -37,6 +37,7 @@ jest.mock("@/components/LegalDocumentContent", () => ({ jest.mock("@/hooks", () => ({ usePageContent: (...args: unknown[]) => mockUsePageContent(...args), + usePageTitle: jest.fn(), })); describe.each([ @@ -67,6 +68,9 @@ describe.each([ render(); expect(mockUsePageContent).toHaveBeenCalledWith(contentKey); + expect(usePageTitle as jest.Mock).toHaveBeenCalledWith( + `${title} – HIV Home Test Service – NHS`, + ); expect(mockLegalDocumentContent).toHaveBeenCalledWith({ content: { ...supplierContent, diff --git a/ui/src/__tests__/content/ContentValidator.test.ts b/ui/src/__tests__/content/ContentValidator.test.ts new file mode 100644 index 000000000..bb0c6a2a0 --- /dev/null +++ b/ui/src/__tests__/content/ContentValidator.test.ts @@ -0,0 +1,189 @@ +import { + assertValidPrivacyPolicyContent, + assertValidTermsOfUseContent, + validateContent, +} from "@/content"; + +const validCommonContent = { + navigation: { back: "Back", continue: "Continue" }, + validation: {}, + links: {}, + errorSummary: {}, + orderStatus: {}, + feedback: {}, + footer: {}, +}; + +const minimalPage = (overrides: Record = {}): Record => ({ + title: "Test page", + pageTitle: "Test page – HIV Home Test Service – NHS", + ...overrides, +}); + +const buildValidPages = (): Record => ({ + "before-you-start": minimalPage(), + "get-self-test-kit-for-HIV": minimalPage(), + "kit-not-available-in-area": minimalPage(), + "go-to-clinic": minimalPage(), + "enter-delivery-address": minimalPage(), + "enter-address-manually": minimalPage(), + "no-address-found": minimalPage(), + "select-delivery-address": minimalPage(), + "how-comfortable-pricking-finger": minimalPage(), + "cannot-use-service-under-18": minimalPage(), + "enter-mobile-phone-number": minimalPage(), + "check-your-answers": minimalPage(), + "confirm-mobile-phone-number": minimalPage(), + "service-error": minimalPage(), + "order-tracking": minimalPage(), + "test-results": minimalPage(), + "blood-sample-guide": minimalPage(), + "order-submitted": minimalPage(), + "suppliers-terms-conditions": { title: "Supplier terms", suppliers: {} }, + "suppliers-privacy-policy": { title: "Supplier privacy", suppliers: {} }, +}); + +describe("ContentValidator", () => { + describe("validateContent – pageTitle on main pages", () => { + it("returns valid when all pages have pageTitle", () => { + const result = validateContent({ + commonContent: validCommonContent, + pages: buildValidPages(), + }); + + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("returns an error when a main page is missing pageTitle", () => { + const pages = buildValidPages(); + const page = pages["before-you-start"] as Record; + delete page["pageTitle"]; + + const result = validateContent({ + commonContent: validCommonContent, + pages, + }); + + expect(result.valid).toBe(false); + expect(result.errors).toContain( + "pages.before-you-start.pageTitle must be a non-empty string", + ); + }); + + it("returns an error when a main page has a blank pageTitle", () => { + const pages = buildValidPages(); + (pages["enter-delivery-address"] as Record)["pageTitle"] = " "; + + const result = validateContent({ + commonContent: validCommonContent, + pages, + }); + + expect(result.valid).toBe(false); + expect(result.errors).toContain( + "pages.enter-delivery-address.pageTitle must be a non-empty string", + ); + }); + + it.each(["check-your-answers", "confirm-mobile-phone-number", "order-submitted"] as const)( + "validates pageTitle for %s", + (key) => { + const pages = buildValidPages(); + delete (pages[key] as Record)["pageTitle"]; + + const result = validateContent({ + commonContent: validCommonContent, + pages, + }); + + expect(result.errors).toContain(`pages.${key}.pageTitle must be a non-empty string`); + }, + ); + + it("does not require pageTitle on supplier pages", () => { + const result = validateContent({ + commonContent: validCommonContent, + pages: buildValidPages(), + }); + + expect(result.errors.some((e) => e.includes("suppliers-terms-conditions.pageTitle"))).toBe( + false, + ); + expect(result.errors.some((e) => e.includes("suppliers-privacy-policy.pageTitle"))).toBe( + false, + ); + }); + + it("returns an error when order-submitted page is missing", () => { + const pages = buildValidPages(); + delete pages["order-submitted"]; + + const result = validateContent({ + commonContent: validCommonContent, + pages, + }); + + expect(result.valid).toBe(false); + expect(result.errors).toContain("pages is missing required key: order-submitted"); + }); + }); + + describe("assertValidPrivacyPolicyContent", () => { + const validPrivacyPolicy = { + pageTitle: "Home test privacy policy – HIV Home Test Service – NHS", + title: "Privacy Policy", + introduction: [], + sections: [], + }; + + it("does not throw when pageTitle is present", () => { + expect(() => assertValidPrivacyPolicyContent(validPrivacyPolicy)).not.toThrow(); + }); + + it("throws when pageTitle is missing", () => { + const withoutPageTitle = Object.fromEntries( + Object.entries(validPrivacyPolicy).filter(([k]) => k !== "pageTitle"), + ); + + expect(() => assertValidPrivacyPolicyContent(withoutPageTitle)).toThrow( + "pageTitle must be a non-empty string", + ); + }); + + it("throws when pageTitle is blank", () => { + expect(() => + assertValidPrivacyPolicyContent({ ...validPrivacyPolicy, pageTitle: " " }), + ).toThrow("pageTitle must be a non-empty string"); + }); + }); + + describe("assertValidTermsOfUseContent", () => { + const validTerms = { + pageTitle: "Home test terms of use – HIV Home Test Service – NHS", + title: "Terms of Use", + introduction: [], + sections: [], + }; + + it("does not throw when pageTitle is present", () => { + expect(() => assertValidTermsOfUseContent(validTerms)).not.toThrow(); + }); + + it("throws when pageTitle is missing", () => { + const withoutPageTitle = Object.fromEntries( + Object.entries(validTerms).filter(([k]) => k !== "pageTitle"), + ); + + expect(() => assertValidTermsOfUseContent(withoutPageTitle)).toThrow( + "pageTitle must be a non-empty string", + ); + }); + + it("throws when pageTitle is blank", () => { + expect(() => assertValidTermsOfUseContent({ ...validTerms, pageTitle: "" })).toThrow( + "pageTitle must be a non-empty string", + ); + }); + }); +}); diff --git a/ui/src/__tests__/routes/CallbackPage.test.tsx b/ui/src/__tests__/routes/CallbackPage.test.tsx index d94d07873..9b0acc711 100644 --- a/ui/src/__tests__/routes/CallbackPage.test.tsx +++ b/ui/src/__tests__/routes/CallbackPage.test.tsx @@ -49,6 +49,9 @@ jest.mock("@/hooks", () => ({ } }; }, + usePageTitle: (title: string) => { + document.title = title; + }, })); const mockedConsumeLoginCsrf = jest.mocked(consumeLoginCsrf); @@ -74,6 +77,7 @@ describe("CallbackPage", () => { render(); }); + expect(document.title).toBe("Signing you in – HIV Home Test Service – NHS"); expect(mockedLoginService.login).not.toHaveBeenCalled(); expect(mockSetUser).not.toHaveBeenCalled(); expect(mockNavigate).not.toHaveBeenCalled(); diff --git a/ui/src/__tests__/routes/HomeTestPrivacyPolicyPage.test.tsx b/ui/src/__tests__/routes/HomeTestPrivacyPolicyPage.test.tsx index 39313e216..5d05cad4d 100644 --- a/ui/src/__tests__/routes/HomeTestPrivacyPolicyPage.test.tsx +++ b/ui/src/__tests__/routes/HomeTestPrivacyPolicyPage.test.tsx @@ -134,4 +134,8 @@ describe("HomeTestPrivacyPolicyPage", () => { expect(paragraphs[0].closest("p")).toHaveClass("nhsuk-body"); }); }); + + it("sets the document title", () => { + expect(document.title).toBe("Home test privacy policy – HIV Home Test Service – NHS"); + }); }); diff --git a/ui/src/__tests__/routes/HomeTestTermsOfUsePage.test.tsx b/ui/src/__tests__/routes/HomeTestTermsOfUsePage.test.tsx index acbcc9b22..fedef59f7 100644 --- a/ui/src/__tests__/routes/HomeTestTermsOfUsePage.test.tsx +++ b/ui/src/__tests__/routes/HomeTestTermsOfUsePage.test.tsx @@ -199,4 +199,8 @@ describe("HomeTestTermsOfUsePage", () => { expect(lists.length).toBeGreaterThan(0); }); }); + + it("sets the document title", () => { + expect(document.title).toBe("Home test terms of use – HIV Home Test Service – NHS"); + }); }); diff --git a/ui/src/__tests__/routes/LoginPage.test.tsx b/ui/src/__tests__/routes/LoginPage.test.tsx index 7aa06f590..be38f6ae0 100644 --- a/ui/src/__tests__/routes/LoginPage.test.tsx +++ b/ui/src/__tests__/routes/LoginPage.test.tsx @@ -94,6 +94,8 @@ describe("LoginPage", () => { const { default: LoginPage } = await import("@/routes/LoginPage"); render(); + expect(document.title).toBe("Sign in – HIV Home Test Service – NHS"); + await waitFor(() => { expect(mockedGetAuthorizeLoginHintFragment).toHaveBeenCalledWith("user@example.com"); }); @@ -109,6 +111,8 @@ describe("LoginPage", () => { render(); + expect(document.title).toBe("Sign in – HIV Home Test Service – NHS"); + await waitFor(() => { expect(mockedGenerateState).toHaveBeenCalledWith("/"); }); @@ -123,6 +127,8 @@ describe("LoginPage", () => { render(); + expect(document.title).toBe("Sign in – HIV Home Test Service – NHS"); + await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith(RoutePath.ServiceErrorPage, { replace: true, diff --git a/ui/src/__tests__/routes/OrderTrackingPage.test.tsx b/ui/src/__tests__/routes/OrderTrackingPage.test.tsx index f96fbc234..0c8b167ef 100644 --- a/ui/src/__tests__/routes/OrderTrackingPage.test.tsx +++ b/ui/src/__tests__/routes/OrderTrackingPage.test.tsx @@ -290,4 +290,10 @@ describe("OrderTrackingPage", () => { expect(screen.queryByTestId("about-service")).not.toBeInTheDocument(); }); }); + + it("sets the document title", () => { + renderWithRouter("abc123"); + + expect(document.title).toBe("Track your order – HIV Home Test Service – NHS"); + }); }); diff --git a/ui/src/__tests__/routes/PageTitle.integration.test.tsx b/ui/src/__tests__/routes/PageTitle.integration.test.tsx new file mode 100644 index 000000000..e719f35ce --- /dev/null +++ b/ui/src/__tests__/routes/PageTitle.integration.test.tsx @@ -0,0 +1,82 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; + +import SuppliersPrivacyPolicyPage from "@/routes/SuppliersPrivacyPolicyPage"; +import SuppliersTermsConditionsPage from "@/routes/SuppliersTermsConditionsPage"; +import BeforeYouStartPage from "@/routes/get-self-test-kit-for-HIV-journey/BeforeYouStartPage"; +import GetSelfTestKitPage from "@/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage"; +import { CreateOrderProvider, JourneyNavigationProvider } from "@/state"; + +function renderJourneyRoutes(initialEntry: string) { + return render( + + + + + } /> + } /> + + + + , + ); +} + +function renderSupplierPrivacyPolicyRoute(url: string) { + return render( + + + } /> + + , + ); +} + +function renderSupplierTermsConditionsRoute(url: string) { + return render( + + + } /> + + , + ); +} + +describe("page titles", () => { + it("updates the document title when navigating between journey views", async () => { + renderJourneyRoutes("/before-you-start"); + + expect(document.title).toBe( + "Before you order a free HIV self-test kit – HIV Home Test Service – NHS", + ); + + fireEvent.click(screen.getByRole("button", { name: "Continue to order a kit" })); + + await waitFor(() => { + expect(document.title).toBe("Order a free HIV self-test kit – HIV Home Test Service – NHS"); + }); + + expect( + screen.getByRole("heading", { name: "Order a free HIV self-test kit", level: 1 }), + ).toBeInTheDocument(); + }); + + it("sets the document title for the supplier privacy policy view", () => { + renderSupplierPrivacyPolicyRoute("/suppliers-privacy-policy?supplier=preventx"); + + expect(document.title).toBe("Preventx privacy policy – HIV Home Test Service – NHS"); + expect( + screen.getByRole("heading", { name: "Preventx privacy policy", level: 1 }), + ).toBeInTheDocument(); + }); + + it("sets the document title for the supplier terms of use view", () => { + renderSupplierTermsConditionsRoute("/suppliers-terms-conditions?supplier=preventx"); + + expect(document.title).toBe("Preventx terms of use – HIV Home Test Service – NHS"); + expect( + screen.getByRole("heading", { name: "Preventx terms of use", level: 1 }), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/src/__tests__/routes/ServiceErrorPage.test.tsx b/ui/src/__tests__/routes/ServiceErrorPage.test.tsx index b444db0a4..23163b472 100644 --- a/ui/src/__tests__/routes/ServiceErrorPage.test.tsx +++ b/ui/src/__tests__/routes/ServiceErrorPage.test.tsx @@ -1,16 +1,19 @@ import "@testing-library/jest-dom"; - -import { MemoryRouter } from "react-router-dom"; import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; + +import { usePageTitle } from "@/hooks"; import ServiceErrorPage from "@/routes/ServiceErrorPage"; jest.mock("@/hooks", () => ({ useContent: () => ({ "service-error": { + pageTitle: "Sorry, there is a problem with the service – HIV Home Test Service – NHS", title: "Sorry, there is a problem with the service", message: "Try again later.", }, }), + usePageTitle: jest.fn(), })); jest.mock("@/layouts/PageLayout", () => ({ @@ -54,10 +57,7 @@ describe("ServiceErrorPage", () => { renderWithState({ errorMessage: "Something went wrong" }); - expect(consoleSpy).toHaveBeenCalledWith( - "[ServiceErrorPage]", - "Something went wrong", - ); + expect(consoleSpy).toHaveBeenCalledWith("[ServiceErrorPage]", "Something went wrong"); consoleSpy.mockRestore(); }); @@ -71,4 +71,12 @@ describe("ServiceErrorPage", () => { consoleSpy.mockRestore(); }); + + it("sets the page title", () => { + renderWithState(); + + expect(usePageTitle as jest.Mock).toHaveBeenCalledWith( + "Sorry, there is a problem with the service – HIV Home Test Service – NHS", + ); + }); }); diff --git a/ui/src/__tests__/routes/TestResultsPage.test.tsx b/ui/src/__tests__/routes/TestResultsPage.test.tsx index 4b375075b..66b70b6b0 100644 --- a/ui/src/__tests__/routes/TestResultsPage.test.tsx +++ b/ui/src/__tests__/routes/TestResultsPage.test.tsx @@ -1,16 +1,15 @@ -import "@testing-library/jest-dom"; - -import { AuthUser, useAuth } from "@/state"; -import { MemoryRouter, Route, Routes } from "react-router-dom"; -import { OrderDetails, OrderStatus } from "@/lib/models/order-details"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import "@testing-library/jest-dom"; import { render, screen, waitFor } from "@testing-library/react"; -import { TestErrorBoundary } from "@/lib/test-utils/TestErrorBoundary"; - -import TestResultsPage from "@/routes/TestResultsPage"; import { act } from "react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; + +import { OrderDetails, OrderStatus } from "@/lib/models/order-details"; import orderDetailsService from "@/lib/services/order-details-service"; import testResultsService from "@/lib/services/test-results-service"; +import { TestErrorBoundary } from "@/lib/test-utils/TestErrorBoundary"; +import TestResultsPage from "@/routes/TestResultsPage"; +import { AuthUser, useAuth } from "@/state"; jest.mock("@/lib/services/order-details-service", () => ({ __esModule: true, @@ -243,4 +242,10 @@ describe("TestResultsPage", () => { expect(screen.getByText("Failed to fetch results")).toBeInTheDocument(); }); }); + + it("sets the document title", () => { + renderWithRouter("abc123"); + + expect(document.title).toBe("HIV self-test result – HIV Home Test Service – NHS"); + }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/BeforeYouStartPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/BeforeYouStartPage.test.tsx index cc2bc4f38..d84bef1c4 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/BeforeYouStartPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/BeforeYouStartPage.test.tsx @@ -108,6 +108,8 @@ describe("BeforeYouStartPage", () => { it("sets the document title", () => { render(, { wrapper: TestWrapper }); - expect(document.title).toBe("Before you order a free HIV self-test kit - HomeTest - NHS"); + expect(document.title).toBe( + "Before you order a free HIV self-test kit – HIV Home Test Service – NHS", + ); }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/BloodSampleGuidePage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/BloodSampleGuidePage.test.tsx index 903602239..4b1112309 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/BloodSampleGuidePage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/BloodSampleGuidePage.test.tsx @@ -133,4 +133,8 @@ describe("BloodSampleGuidePage", () => { screen.getByText(/Place your blood sample tube into the protective packaging/), ).toBeInTheDocument(); }); + + it("sets the document title", () => { + expect(document.title).toBe("Blood sample step-by-step guide – HIV Home Test Service – NHS"); + }); }); 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 f184ccf12..62ec02a41 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 @@ -4,6 +4,7 @@ import React, { ReactNode } from "react"; import { MemoryRouter } from "react-router-dom"; import { CannotUseServiceUnder18Content, CommonContent } from "@/content"; +import { usePageTitle } from "@/hooks"; import { JourneyStepNames, RoutePath } from "@/lib/models/route-paths"; import CannotUseServiceUnder18Page, { HARD_CODED_CLINIC_DATA, @@ -17,6 +18,7 @@ import { } from "@/state"; const mockedContent: CannotUseServiceUnder18Content = { + pageTitle: "You cannot use this service as you are under 18 – HIV Home Test Service – NHS", title: "some-mocked-title", intro: "some-mocked-intro", phoneLabel: "some-mocked-phone-label", @@ -42,6 +44,7 @@ jest.mock("@/hooks", () => ({ "cannot-use-service-under-18": mockedContent, }), useCommonContent: () => mockedCommonContent, + usePageTitle: jest.fn(), })); const goBackMock = jest.fn(); @@ -268,3 +271,13 @@ describe("Back Navigation", () => { }); }); }); + +describe("CannotUseServiceUnder18Page page title", () => { + it("sets the page title", () => { + renderWithProviders(); + + expect(usePageTitle as jest.Mock).toHaveBeenCalledWith( + "You cannot use this service as you are under 18 – HIV Home Test Service – NHS", + ); + }); +}); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/CheckYourAnswersPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/CheckYourAnswersPage.test.tsx index 08c608cde..c2ce5bf3d 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/CheckYourAnswersPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/CheckYourAnswersPage.test.tsx @@ -181,6 +181,14 @@ describe("CheckYourAnswersPage", () => { expect(heading).toBeInTheDocument(); }); + it("sets the document title", () => { + render(, { wrapper: TestWrapper }); + + expect(document.title).toBe( + "Check your answers before submitting your order – HIV Home Test Service – NHS", + ); + }); + it("renders the update message", () => { render(, { wrapper: TestWrapper }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/ConfirmMobileNumberPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/ConfirmMobileNumberPage.test.tsx index 074155e08..7a80d83b9 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/ConfirmMobileNumberPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/ConfirmMobileNumberPage.test.tsx @@ -69,7 +69,7 @@ describe("ConfirmMobileNumberPage", () => { render(, { wrapper: TestWrapper }); const heading = screen.getByRole("heading", { - name: /what's your mobile phone number\?/i, + name: /confirm your mobile phone number/i, }); expect(heading).toBeInTheDocument(); }); @@ -532,7 +532,7 @@ describe("ConfirmMobileNumberPage", () => { expect( screen.getByRole("heading", { - name: /what's your mobile phone number\?/i, + name: /confirm your mobile phone number/i, }), ).toBeInTheDocument(); @@ -560,4 +560,10 @@ describe("ConfirmMobileNumberPage", () => { ).not.toBeInTheDocument(); }); }); + + it("sets the document title", () => { + render(, { wrapper: TestWrapper }); + + expect(document.title).toBe("Confirm your mobile phone number – HIV Home Test Service – NHS"); + }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterAddressManuallyPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterAddressManuallyPage.test.tsx index 2783c7159..6cb42f55f 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterAddressManuallyPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterAddressManuallyPage.test.tsx @@ -110,6 +110,14 @@ describe("EnterAddressManuallyPage", () => { ).toBeInTheDocument(); }); + it("sets the document title", () => { + render(, { wrapper: TestWrapper }); + + expect(document.title).toBe( + "Enter your delivery address manually and we'll check if the kit's available – HIV Home Test Service – NHS", + ); + }); + it("renders all form elements", () => { render(, { wrapper: TestWrapper }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterDeliveryAddressPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterDeliveryAddressPage.test.tsx index d9e24ed33..eb2c3d57e 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterDeliveryAddressPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterDeliveryAddressPage.test.tsx @@ -1,12 +1,11 @@ import "@testing-library/jest-dom"; - import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; - import React from "react"; +import { MemoryRouter } from "react-router-dom"; + import { TestErrorBoundary } from "@/lib/test-utils/TestErrorBoundary"; -import { CreateOrderProvider, JourneyNavigationProvider, PostcodeLookupProvider } from "@/state"; import EnterDeliveryAddressPage from "@/routes/get-self-test-kit-for-HIV-journey/EnterDeliveryAddressPage"; -import { MemoryRouter } from "react-router-dom"; +import { CreateOrderProvider, JourneyNavigationProvider, PostcodeLookupProvider } from "@/state"; const mockLookupPostcode = jest.fn(); const mockClearAddresses = jest.fn(); @@ -55,6 +54,14 @@ describe("EnterDeliveryAddressPage", () => { expect(heading).toBeInTheDocument(); }); + it("sets the document title", () => { + render(, { wrapper: TestWrapper }); + + expect(document.title).toBe( + "Enter your delivery address and we'll check if the kit's available – HIV Home Test Service – NHS", + ); + }); + it("renders all form elements", () => { render(, { wrapper: TestWrapper }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterMobileNumberPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterMobileNumberPage.test.tsx index 90fc40a64..2010f2923 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterMobileNumberPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/EnterMobileNumberPage.test.tsx @@ -484,4 +484,10 @@ describe("EnterMobileNumberPage", () => { expect(mobileInput.value).toBe("07771 900 900"); }); }); + + it("sets the document title", () => { + render(, { wrapper: TestWrapper }); + + expect(document.title).toBe("What's your mobile phone number? – HIV Home Test Service – NHS"); + }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage.test.tsx index 0d7a0882d..6c80c0120 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/GetSelfTestKitPage.test.tsx @@ -77,7 +77,7 @@ describe("GetSelfTestKitPage", () => { }); it("sets the document title", () => { - expect(document.title).toBe("Order a free HIV self-test kit - HomeTest - NHS"); + expect(document.title).toBe("Order a free HIV self-test kit – HIV Home Test Service – NHS"); }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage.test.tsx index 76bb71d70..230707050 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage.test.tsx @@ -1,9 +1,8 @@ import "@testing-library/jest-dom"; - import { fireEvent, render, screen } from "@testing-library/react"; +import { usePageContent, usePageTitle } from "@/hooks"; import GoToClinicPage from "@/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage"; -import { usePageContent } from "@/hooks"; const mockGoToStep = jest.fn(); const mockGoBack = jest.fn(); @@ -27,6 +26,7 @@ jest.mock("@/state", () => ({ jest.mock("@/hooks", () => ({ usePageContent: jest.fn(), + usePageTitle: jest.fn(), })); jest.mock("@/layouts/FormPageLayout", () => ({ @@ -69,6 +69,7 @@ describe("GoToClinicPage", () => { mockNavigationContext.stepHistory = ["how-comfortable-pricking-finger", "go-to-clinic"]; (usePageContent as jest.Mock).mockReturnValue({ + pageTitle: "Contact your nearest sexual health clinic – HIV Home Test Service – NHS", title: "Contact your nearest sexual health clinic", moreOptionsHeading: "More options and information", }); @@ -109,4 +110,12 @@ describe("GoToClinicPage", () => { expect(mockGoBack).not.toHaveBeenCalled(); }); }); + + it("sets the page title", () => { + render(); + + expect(usePageTitle as jest.Mock).toHaveBeenCalledWith( + "Contact your nearest sexual health clinic – HIV Home Test Service – NHS", + ); + }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage.test.tsx index d68408b6f..223fcf8d5 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { useEffect } from "react"; import { MemoryRouter, useLocation } from "react-router-dom"; +import { usePageTitle } from "@/hooks"; import HowComfortablePrickingFingerPage from "@/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage"; import { AuthProvider, CreateOrderProvider, JourneyNavigationProvider, useAuth } from "@/state"; @@ -28,6 +29,8 @@ jest.mock("@/hooks", () => ({ }, }, "how-comfortable-pricking-finger": { + pageTitle: + "This is what you'll need to do to give a blood sample – HIV Home Test Service – NHS", title: "This is what you'll need to do to give a blood sample", instructions: "To give a sample of blood, you'll need to:", steps: { @@ -50,6 +53,7 @@ jest.mock("@/hooks", () => ({ }, }, }), + usePageTitle: jest.fn(), })); const TestWrapper = ({ children }: { children: React.ReactNode }) => ( @@ -337,4 +341,12 @@ describe("HowComfortablePrickingFingerPage", () => { ); }); }); + + it("sets the page title", () => { + render(, { wrapper: TestWrapper }); + + expect(usePageTitle as jest.Mock).toHaveBeenCalledWith( + "This is what you'll need to do to give a blood sample – HIV Home Test Service – NHS", + ); + }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage.test.tsx index 519ba233a..891084a52 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage.test.tsx @@ -1,9 +1,8 @@ import "@testing-library/jest-dom"; - import { fireEvent, render, screen } from "@testing-library/react"; +import { usePageContent, usePageTitle } from "@/hooks"; import KitNotAvailableInAreaPage from "@/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage"; -import { usePageContent } from "@/hooks"; const mockGoToStep = jest.fn(); const mockGoBack = jest.fn(); @@ -27,6 +26,7 @@ jest.mock("@/state", () => ({ jest.mock("@/hooks", () => ({ usePageContent: jest.fn(), + usePageTitle: jest.fn(), })); jest.mock("@/layouts/FormPageLayout", () => ({ @@ -69,6 +69,8 @@ describe("KitNotAvailableInAreaPage", () => { mockNavigationContext.stepHistory = ["enter-delivery-address", "kit-not-available-in-area"]; (usePageContent as jest.Mock).mockReturnValue({ + pageTitle: + "Free HIV self-test kits are not available in your area using this service – HIV Home Test Service – NHS", title: "Free HIV self-test kits are not available in your area using this service", description: "There are other options to get an HIV test.", moreOptionsHeading: "More options and information", @@ -110,4 +112,12 @@ describe("KitNotAvailableInAreaPage", () => { expect(mockGoBack).not.toHaveBeenCalled(); }); }); + + it("sets the page title", () => { + render(); + + expect(usePageTitle as jest.Mock).toHaveBeenCalledWith( + "Free HIV self-test kits are not available in your area using this service – HIV Home Test Service – NHS", + ); + }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/NoAddressFoundPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/NoAddressFoundPage.test.tsx index 813140ece..5c97e7551 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/NoAddressFoundPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/NoAddressFoundPage.test.tsx @@ -42,4 +42,10 @@ describe("NoAddressFoundPage", () => { expect(link.tagName).toBe("A"); }); }); + + it("sets the document title", () => { + render(, { wrapper: TestWrapper }); + + expect(document.title).toBe("No address found – HIV Home Test Service – NHS"); + }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/OrderSubmittedPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/OrderSubmittedPage.test.tsx index 64c2db26f..bb499fb22 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/OrderSubmittedPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/OrderSubmittedPage.test.tsx @@ -241,4 +241,10 @@ describe("OrderSubmittedPage", () => { expect(screen.getByText(/ORD-12345-TEST/i)).toBeInTheDocument(); }); }); + + it("sets the document title", () => { + render(, { wrapper: TestWrapper }); + + expect(document.title).toBe("Order submitted – HIV Home Test Service – NHS"); + }); }); diff --git a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/SelectDeliveryAddressPage.test.tsx b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/SelectDeliveryAddressPage.test.tsx index 4b2b069ad..8fdadfd55 100644 --- a/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/SelectDeliveryAddressPage.test.tsx +++ b/ui/src/__tests__/routes/get-self-test-kit-for-HIV-journey/SelectDeliveryAddressPage.test.tsx @@ -131,6 +131,7 @@ jest.mock("@/hooks/useContent", () => ({ }, }, "select-delivery-address": { + pageTitle: "Select your delivery address – HIV Home Test Service – NHS", title: "found", postcodeLabel: "Postcode:", editPostcodeLink: "Edit postcode", @@ -205,6 +206,10 @@ describe("SelectDeliveryAddressPage", () => { expect(heading).toHaveTextContent("3 addresses found"); }); + it("sets the document title", () => { + expect(document.title).toBe("3 addresses found – HIV Home Test Service – NHS"); + }); + it("displays the searched postcode", () => { expect(screen.getByText(/postcode:/i)).toBeInTheDocument(); }); diff --git a/ui/src/components/SupplierLegalDocumentContent.tsx b/ui/src/components/SupplierLegalDocumentContent.tsx index 44fc500c5..0603ef0b0 100644 --- a/ui/src/components/SupplierLegalDocumentContent.tsx +++ b/ui/src/components/SupplierLegalDocumentContent.tsx @@ -1,5 +1,6 @@ import { LegalDocumentContent } from "@/components/LegalDocumentContent"; -import { usePageContent } from "@/hooks"; +import { usePageContent, usePageTitle } from "@/hooks"; +import { formatPageTitle } from "@/lib/utils/page-title"; type SupplierLegalDocumentType = "terms" | "privacy"; @@ -40,5 +41,7 @@ export function SupplierLegalDocumentContent({ const supplierContent = content.suppliers[normalizedSupplier]; + usePageTitle(formatPageTitle(supplierContent.title)); + return ; } diff --git a/ui/src/lib/utils/page-title.ts b/ui/src/lib/utils/page-title.ts index b75b6dd30..03fc8d1ae 100644 --- a/ui/src/lib/utils/page-title.ts +++ b/ui/src/lib/utils/page-title.ts @@ -1 +1,7 @@ -export const DEFAULT_PAGE_TITLE = "NHS HIV Home Test Service"; +export const SERVICE_NAME = "HIV Home Test Service – NHS"; + +export const DEFAULT_PAGE_TITLE = SERVICE_NAME; + +export function formatPageTitle(pageName: string): string { + return `${pageName} – ${SERVICE_NAME}`; +} diff --git a/ui/src/routes/CallbackPage.tsx b/ui/src/routes/CallbackPage.tsx index 8b313e26e..b49595f56 100644 --- a/ui/src/routes/CallbackPage.tsx +++ b/ui/src/routes/CallbackPage.tsx @@ -3,10 +3,11 @@ import { useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; -import { useAsyncErrorHandler } from "@/hooks"; +import { useAsyncErrorHandler, usePageTitle } from "@/hooks"; import { consumeLoginCsrf, verifyState } from "@/lib/auth/loginState"; import { RoutePath } from "@/lib/models/route-paths"; import loginService from "@/lib/services/login-service"; +import { formatPageTitle } from "@/lib/utils/page-title"; import { useAuth } from "@/state"; function safeReturnTo(value: string | null | undefined) { @@ -36,6 +37,9 @@ export default function CallbackPage() { const { setUser } = useAuth(); const navigate = useNavigate(); const didRun = useRef(false); + + usePageTitle(formatPageTitle("Signing you in")); + const handleCallback = useAsyncErrorHandler(async () => { const params = new URLSearchParams(globalThis.location.search); const code = params.get("code"); diff --git a/ui/src/routes/HomeTestPrivacyPolicyPage.tsx b/ui/src/routes/HomeTestPrivacyPolicyPage.tsx index 8c2ac8711..d4432c613 100644 --- a/ui/src/routes/HomeTestPrivacyPolicyPage.tsx +++ b/ui/src/routes/HomeTestPrivacyPolicyPage.tsx @@ -4,13 +4,14 @@ import "@/styles/lists.css"; import { useNavigate } from "react-router-dom"; -import { useContent } from "@/hooks"; +import { useContent, usePageTitle } 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(); + usePageTitle(content.pageTitle); const renderHeading = (text: string) => { const numberMatch = /^(\d+\.\s+)/.exec(text); diff --git a/ui/src/routes/HomeTestTermsOfUsePage.tsx b/ui/src/routes/HomeTestTermsOfUsePage.tsx index bddb5ca33..597bbe1f9 100644 --- a/ui/src/routes/HomeTestTermsOfUsePage.tsx +++ b/ui/src/routes/HomeTestTermsOfUsePage.tsx @@ -5,7 +5,7 @@ import "@/styles/lists.css"; import { useNavigate } from "react-router-dom"; import type { PrivacyPolicySection, PrivacyPolicySubsection } from "@/content"; -import { useContent } from "@/hooks"; +import { useContent, usePageTitle } from "@/hooks"; import PageLayout from "@/layouts/PageLayout"; import { cleanListItems, getListClass, renderTextWithLinks } from "@/utils/renderTextWithLinks"; @@ -120,6 +120,7 @@ const renderSection = (section: PrivacyPolicySection) => ( export default function HomeTestTermsOfUsePage() { const navigate = useNavigate(); const { "home-test-terms-of-use": content } = useContent(); + usePageTitle(content.pageTitle); return ( navigate(-1)}> diff --git a/ui/src/routes/LoginPage.tsx b/ui/src/routes/LoginPage.tsx index c1b303a9c..65eae5cd6 100644 --- a/ui/src/routes/LoginPage.tsx +++ b/ui/src/routes/LoginPage.tsx @@ -3,14 +3,18 @@ import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import { usePageTitle } from "@/hooks"; import { getAuthorizeLoginHintFragment } from "@/lib/auth/loginHint"; import { generateState, persistLoginCsrf } from "@/lib/auth/loginState"; import { RoutePath } from "@/lib/models/route-paths"; +import { formatPageTitle } from "@/lib/utils/page-title"; import * as settings from "@/settings"; export default function RedirectPage() { const navigate = useNavigate(); + usePageTitle(formatPageTitle("Sign in")); + useEffect(() => { const authorizeUrl = settings.nhsLoginAuthorizeUrl?.trim(); const clientId = settings.nhsLoginClientId?.trim(); diff --git a/ui/src/routes/OrderTrackingPage.tsx b/ui/src/routes/OrderTrackingPage.tsx index 6a9131aec..1365f6618 100644 --- a/ui/src/routes/OrderTrackingPage.tsx +++ b/ui/src/routes/OrderTrackingPage.tsx @@ -4,7 +4,7 @@ import { useParams } from "react-router-dom"; import { AboutService } from "@/components/AboutService"; import { OrderStatus } from "@/components/order-status"; -import { usePageContent } from "@/hooks"; +import { usePageContent, usePageTitle } from "@/hooks"; import PageLayout from "@/layouts/PageLayout"; import { Patient } from "@/lib/models/patient"; import { useOrderStatusQuery } from "@/lib/queries/order-status-query"; @@ -48,6 +48,7 @@ export default function OrderTrackingPage() { const { orderId } = useParams<{ orderId: string }>(); const { user } = useAuth(); const content = usePageContent("order-tracking"); + usePageTitle(content.pageTitle); if (!orderId || !isValidGuid(orderId)) { return ( diff --git a/ui/src/routes/ServiceErrorPage.tsx b/ui/src/routes/ServiceErrorPage.tsx index 14d92c3b7..33c5c5934 100644 --- a/ui/src/routes/ServiceErrorPage.tsx +++ b/ui/src/routes/ServiceErrorPage.tsx @@ -1,9 +1,11 @@ -import PageLayout from "@/layouts/PageLayout"; -import { useContent } from "@/hooks"; import { useLocation } from "react-router-dom"; +import { useContent, usePageTitle } from "@/hooks"; +import PageLayout from "@/layouts/PageLayout"; + export default function ServiceErrorPage() { const { "service-error": content } = useContent(); + usePageTitle(content.pageTitle); const { state } = useLocation(); if (state?.errorMessage) { diff --git a/ui/src/routes/TestResultsPage.tsx b/ui/src/routes/TestResultsPage.tsx index 9c0a7dad7..8396d25ee 100644 --- a/ui/src/routes/TestResultsPage.tsx +++ b/ui/src/routes/TestResultsPage.tsx @@ -1,16 +1,16 @@ -import { OrderDetails, OrderStatus } from "@/lib/models/order-details"; +import { useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { NegativeTestResult } from "@/components/test-results/NegativeTestResult"; +import { usePageContent, usePageTitle } from "@/hooks"; import PageLayout from "@/layouts/PageLayout"; +import { OrderDetails, OrderStatus } from "@/lib/models/order-details"; import { Patient } from "@/lib/models/patient"; import { RoutePath } from "@/lib/models/route-paths"; -import { isValidGuid } from "@/lib/utils/guid"; -import { useAuth } from "@/state"; -import { useEffect } from "react"; import { useOrderStatusQuery } from "@/lib/queries/order-status-query"; -import { usePageContent } from "@/hooks"; import { useTestResultsQuery } from "@/lib/queries/test-results-query"; +import { isValidGuid } from "@/lib/utils/guid"; +import { useAuth } from "@/state"; function TestResultsContent({ orderId, @@ -113,6 +113,7 @@ export default function TestResultsPage() { const { orderId } = useParams<{ orderId: string }>(); const { user } = useAuth(); const content = usePageContent("test-results"); + usePageTitle(content.pageTitle); if (!orderId || !isValidGuid(orderId)) { return ( diff --git a/ui/src/routes/get-self-test-kit-for-HIV-journey/BloodSampleGuidePage.tsx b/ui/src/routes/get-self-test-kit-for-HIV-journey/BloodSampleGuidePage.tsx index da709e143..7256abc16 100644 --- a/ui/src/routes/get-self-test-kit-for-HIV-journey/BloodSampleGuidePage.tsx +++ b/ui/src/routes/get-self-test-kit-for-HIV-journey/BloodSampleGuidePage.tsx @@ -2,7 +2,7 @@ import { Details, Images } from "nhsuk-react-components"; -import { useContent } from "@/hooks"; +import { useContent, usePageTitle } from "@/hooks"; import FormPageLayout from "@/layouts/FormPageLayout"; import { RoutePath } from "@/lib/models/route-paths"; import { useJourneyNavigationContext } from "@/state"; @@ -10,6 +10,7 @@ import { useJourneyNavigationContext } from "@/state"; export default function BloodSampleGuidePage() { const { goBack, stepHistory, resetNavigation } = useJourneyNavigationContext(); const { "blood-sample-guide": content } = useContent(); + usePageTitle(content.pageTitle); return ( (null); diff --git a/ui/src/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage.tsx b/ui/src/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage.tsx index b9e64d2f0..2947f6aba 100644 --- a/ui/src/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage.tsx +++ b/ui/src/routes/get-self-test-kit-for-HIV-journey/GoToClinicPage.tsx @@ -1,19 +1,19 @@ "use client"; -import { useCreateOrderContext, useJourneyNavigationContext } from "@/state"; - import { FeedbackSection } from "@/components/FeedbackSection"; import { FindAnotherSexualHealthClinicLink } from "@/components/FindAnotherSexualHealthClinicLink"; -import { JourneyStepNames } from "@/lib/models/route-paths"; import { LearnMoreAboutHivAndAidsLink } from "@/components/LearnMoreAboutHivAndAidsLink"; import { NearestSexualHealthClinicSection } from "@/components/NearestSexualHealthClinicSection"; -import { usePageContent } from "@/hooks"; +import { usePageContent, usePageTitle } from "@/hooks"; import FormPageLayout from "@/layouts/FormPageLayout"; +import { JourneyStepNames } from "@/lib/models/route-paths"; +import { useCreateOrderContext, useJourneyNavigationContext } from "@/state"; export default function GoToClinicPage() { const { goToStep, goBack, stepHistory } = useJourneyNavigationContext(); const { orderAnswers } = useCreateOrderContext(); const content = usePageContent("go-to-clinic"); + usePageTitle(content.pageTitle); return (

{content.title}

- +

{content.moreOptionsHeading}

diff --git a/ui/src/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage.tsx b/ui/src/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage.tsx index 1501dfb64..4bbbfd9c2 100644 --- a/ui/src/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage.tsx +++ b/ui/src/routes/get-self-test-kit-for-HIV-journey/HowComfortablePrickingFingerPage.tsx @@ -3,7 +3,7 @@ import { Button, ErrorSummary, Images, Radios } from "nhsuk-react-components"; import { useState } from "react"; -import { useContent } from "@/hooks"; +import { useContent, usePageTitle } from "@/hooks"; import FormPageLayout from "@/layouts/FormPageLayout"; import { JourneyStepNames } from "@/lib/models/route-paths"; import { useAuth, useCreateOrderContext, useJourneyNavigationContext } from "@/state"; @@ -14,6 +14,7 @@ export default function HowComfortablePrickingFingerPage() { const { orderAnswers, updateOrderAnswers } = useCreateOrderContext(); const { user } = useAuth(); const { commonContent, "how-comfortable-pricking-finger": content } = useContent(); + usePageTitle(content.pageTitle); const [selectedOption, setSelectedOption] = useState( orderAnswers.comfortableDoingTest || "", diff --git a/ui/src/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage.tsx b/ui/src/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage.tsx index 080401834..e10b0719a 100644 --- a/ui/src/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage.tsx +++ b/ui/src/routes/get-self-test-kit-for-HIV-journey/KitNotAvailableInAreaPage.tsx @@ -1,19 +1,19 @@ "use client"; -import { useCreateOrderContext, useJourneyNavigationContext } from "@/state"; - import { FeedbackSection } from "@/components/FeedbackSection"; import { FindAnotherSexualHealthClinicLink } from "@/components/FindAnotherSexualHealthClinicLink"; -import FormPageLayout from "@/layouts/FormPageLayout"; -import { JourneyStepNames } from "@/lib/models/route-paths"; import { LearnMoreAboutHivAndAidsLink } from "@/components/LearnMoreAboutHivAndAidsLink"; import { NearestSexualHealthClinicSection } from "@/components/NearestSexualHealthClinicSection"; -import { usePageContent } from "@/hooks"; +import { usePageContent, usePageTitle } from "@/hooks"; +import FormPageLayout from "@/layouts/FormPageLayout"; +import { JourneyStepNames } from "@/lib/models/route-paths"; +import { useCreateOrderContext, useJourneyNavigationContext } from "@/state"; export default function KitNotAvailableInAreaPage() { const { goToStep, goBack, stepHistory } = useJourneyNavigationContext(); const { orderAnswers } = useCreateOrderContext(); const content = usePageContent("kit-not-available-in-area"); + usePageTitle(content.pageTitle); return ( { if (orderAnswers.orderReferenceNumber == null) { 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 bd578b34a..428c7413f 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 @@ -3,11 +3,12 @@ import { Button, ErrorSummary, Radios } from "nhsuk-react-components"; import { useState } from "react"; -import { useAsyncErrorHandler, useContent } from "@/hooks"; +import { useAsyncErrorHandler, useContent, usePageTitle } 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 { formatPageTitle } from "@/lib/utils/page-title"; import { AddressResult, useAuth, @@ -22,6 +23,11 @@ export default function SelectDeliveryAddressPage() { const { orderAnswers, updateOrderAnswers } = useCreateOrderContext(); const { commonContent, "select-delivery-address": content } = useContent(); const { addresses } = usePostcodeLookup(); + usePageTitle( + formatPageTitle( + `${addresses.length} ${addresses.length === 1 ? "address" : "addresses"} ${content.title}`, + ), + ); const [selectedAddress, setSelectedAddress] = useState( orderAnswers.selectedAddressId || "", ); diff --git a/ui/src/state/PostcodeLookupContext.tsx b/ui/src/state/PostcodeLookupContext.tsx index 05551d2e1..e2d77f325 100644 --- a/ui/src/state/PostcodeLookupContext.tsx +++ b/ui/src/state/PostcodeLookupContext.tsx @@ -60,12 +60,14 @@ const defaultPersistedPostcodeLookupState: PersistedPostcodeLookupState = { error: null, }; -export const PostcodeLookupProvider: React.FC = ({ children }) => { - const [persistedState] = useState(() => - sessionService.rehydratePostcodeLookup( - defaultPersistedPostcodeLookupState, - ), +function getPersistedPostcodeLookupState(): PersistedPostcodeLookupState { + return sessionService.rehydratePostcodeLookup( + defaultPersistedPostcodeLookupState, ); +} + +export const PostcodeLookupProvider: React.FC = ({ children }) => { + const [persistedState] = useState(getPersistedPostcodeLookupState); const [postcode, setPostcode] = useState(persistedState.postcode); const [addresses, setAddresses] = useState(persistedState.addresses); const [selectedAddress, setSelectedAddress] = useState(