diff --git a/.env.local b/.env.local
index b8d02b17..7fcf638a 100644
--- a/.env.local
+++ b/.env.local
@@ -32,7 +32,7 @@ RELAYPATH = "ar-hc-lz-pedw-test-uks-01"
CRMURL = "ts-pedw.crm11.dynamics.com"
CRMURL_VERSION = "v9.2"
-# # //New PreProd
+# //New PreProd
# CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a
# CLIENT_SECRET = VWY8Q~9EtRzGXpd8Xs~5OS1fej2IyVJ8JgTMncw3
# RELAY_ROOT = https://ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-uks-01/
@@ -271,3 +271,4 @@ HIDEERRORLOGS = false
SHOWSIPS = true
ALLOWED_IPS=::1,203.0.113.42,198.51.100.17
+UPLOAD_BATCH_COUNT = 5
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 28014204..4c5f74a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,3 +59,4 @@ GUARDRAILS.md
ai-prompts/
workflows/
AI_CONTEXT.md
+pages/baracuda.min.html
diff --git a/components/breadcrumbs.js b/components/breadcrumbs.js
index 94dffcc5..250e40f3 100644
--- a/components/breadcrumbs.js
+++ b/components/breadcrumbs.js
@@ -12,6 +12,13 @@ import {
setRepresentationCapacity,
setRepresentationSubmit
} from "../store/currentView/action";
+import { resolveCaseBreadcrumbState } from "../lib/routing/routeState";
+import {
+ buildBreadcrumbRendererMaps,
+ resolveMappedRouteRenderer as resolveMappedRouteRendererByMaps
+} from "../lib/routing/breadcrumbRouteMaps";
+import { createMappedRouteRendererGroups } from "../lib/routing/breadcrumbRendererFactories";
+
const Breadcrumbs = (props) => {
const {
currentView,
@@ -69,10 +76,6 @@ const Breadcrumbs = (props) => {
? 9
: "";
- const { va, adv, ads, key } = router.query;
- const isAdv = adv === "true";
- const isAds = ads === "true";
-
const hideServiceNamePaths = [
"/myportal/[appealtypes]",
"/newappeal",
@@ -83,72 +86,205 @@ const Breadcrumbs = (props) => {
const getViewAllLabel = (viewKey) => {
const labelsByKey = {
- awaitingSubmissionDetails: t(
- "myportal:awatitingsubmission-card-title"
- ),
- watchedCases: t("myportal:watchedcases-card-title"),
- myCases: t("myportal:mycases-card-title"),
- myRepresentations: t("myportal:myrepresentations-card-title"),
+ awaitingSubmissionDetails: () =>
+ t("myportal:awatitingsubmission-card-title"),
+ watchedCases: () => t("myportal:watchedcases-card-title"),
+ myCases: () => t("myportal:mycases-card-title"),
+ myRepresentations: () => t("myportal:myrepresentations-card-title"),
mySubmittedReps: "Submitted representations"
};
- return labelsByKey[viewKey] || null;
+ const viewAllLabelResolver = labelsByKey[viewKey];
+ return typeof viewAllLabelResolver === "function"
+ ? viewAllLabelResolver()
+ : viewAllLabelResolver || null;
};
- const breadcrumbHref = (() => {
- if (va === "true") {
- return {
- pathname: "/myportal/viewall",
- query: { key }
- };
- }
-
- const base = session ? "/myportal" : "";
-
- if (!va && !adv && !ads) {
- return {
- pathname: "/myportal"
- };
- }
-
- if (adv === "true") {
- return {
- pathname: `${base}/advancedsearchresults`,
- query: router.query
- };
- }
-
- if (ads === "true") {
- return {
- pathname: `${base}/addresssearchresults`,
- query: router.query
- };
- }
-
- return {
- pathname: `${base}/searchresults`,
- query: router.query
+ const getStaticTextCrumbLabel = (path) => {
+ const staticTextCrumbByPath = {
+ "/account/register": () =>
+ t("account:register-new-account-heading"),
+ "/dns": () => t("dnsCommon:service-name"),
+ "/dns/application-process": () => "Guidance",
+ "/dns/help": () => "Help",
+ "/dns/contact-us": () => t("common:footer-contact-us-link-label"),
+ "/dns/applications": () => t("dnsApplications:page-title"),
+ "/help/cookies": () => t("cookies:cookie-breadcrumb"),
+ "/privacy": () => t("common:footer-privacy-link-label"),
+ "/accessibility": () => t("common:footer-accessibility-link-label"),
+ "/details-about-cookies": () =>
+ t("cookies:cookie-policy-title-heading"),
+ "/auth/signin": () => t("auth:auth-page-title"),
+ "/auth/verify-request": () => t("auth:auth-check-email-page-title"),
+ "/auth/error": () => t("auth:auth-error-signin-error-title")
};
- })();
- const breadcrumbLabel = (() => {
- if (va === "true") {
- return getViewAllLabel(key);
- }
+ const staticTextCrumbResolver = staticTextCrumbByPath[path];
+ return staticTextCrumbResolver ? staticTextCrumbResolver() : null;
+ };
- const keyedLabel = getViewAllLabel(key);
- if (keyedLabel) return keyedLabel;
+ const renderMyPortalCrumb = () => (
+
+
+ {t("common:breadcrumb-my-portal")}
+
+
+ );
- if (isAdv) {
- return t("common:breadcrumb-advanced-search-results");
- }
+ const renderMyPortalSectionCrumbs = (sectionLabel) => (
+ <>
+ {renderMyPortalCrumb()}
+ {renderTextCrumb(sectionLabel)}
+ >
+ );
- if (isAds) {
- return t("common:breadcrumb-address-search-results");
- }
+ const renderCaseReferenceCrumb = (referenceValue) => (
+
+ {t("common:breadcrumb-case-reference")}: {referenceValue}
+
+ );
+
+ const renderTextCrumb = (label) => (
+ {label}
+ );
+
+ const renderLinkCrumb = (href, label, onClick) => (
+
+
+ {label}
+
+
+ );
+
+ const renderAnchorCrumb = (
+ href,
+ label,
+ onClick,
+ listItemClass = "govuk-breadcrumbs__list-item"
+ ) => (
+
+
+ {label}
+
+
+ );
+
+ const renderBackCrumb = (
+ onClick,
+ listItemClass = "govuk-breadcrumbs__list-item backChevron"
+ ) => renderAnchorCrumb("#", t("common:back-link"), onClick, listItemClass);
+
+ const renderDnsCaseReferenceCrumbs = (referenceValue) => (
+ <>
+ {renderLinkCrumb(
+ isWelsh
+ ? `${router.locale}/dnsapplications`
+ : "/dnsapplications",
+ t("dnsCommon:service-name")
+ )}
+ {renderCaseReferenceCrumb(referenceValue)}
+ >
+ );
+
+ const resolveMappedRouteRenderer = (path) => {
+ return resolveMappedRouteRendererByMaps(path, mappedRouteRendererMaps);
+ };
+
+ const renderMappedRoute = (path, resolver = resolveMappedRouteRenderer) => {
+ const routeRenderer = resolver(path);
+ return routeRenderer ? routeRenderer() : null;
+ };
+
+ const { breadcrumbHref, caseResultsHref, breadcrumbLabel } =
+ resolveCaseBreadcrumbState({
+ query: router.query,
+ hasSession: Boolean(session),
+ getViewAllLabel,
+ advancedLabel: t("common:breadcrumb-advanced-search-results"),
+ addressLabel: t("common:breadcrumb-address-search-results"),
+ defaultLabel: t("common:breadcrumb-search-results")
+ });
+
+ const {
+ simpleRouteRenderersByPath,
+ simpleLinkTextPairRenderersByPath,
+ simpleMyPortalRouteRenderersByPath,
+ newAppealRouteRenderersByPath,
+ callbackRouteRenderersByPath,
+ stepBackRouteRenderersByPath,
+ representationRouteRenderersByPath,
+ caseDetailRouteRenderersByPath,
+ detailAndAccountRouteRenderersByPath
+ } = createMappedRouteRendererGroups({
+ t,
+ router,
+ isWelsh,
+ myPortalHref,
+ cyMyPortalHref,
+ currentViewState,
+ getViewAllLabel,
+ caseResultsHref,
+ breadcrumbLabel,
+ fallbackSearchTitle,
+ currentReference,
+ nestedSearchString,
+ caseReferenceDisplay,
+ onBack: () => {
+ router.back();
+ },
+ currentSection: appealType.currentSection,
+ onStepBack: () => {
+ setCurrentSection(appealType.currentSection - 1);
+ },
+ onStepBackWithInlineClass: () => {
+ setCurrentSection(appealType.currentSection - 1);
+ },
+ currentView,
+ hasRepDetails,
+ repDetails,
+ showQuestionnaireSection,
+ onRepresentationSubmitBackFromConfirmation: () => {
+ setRepresentationSubmit(qcount);
+ },
+ onRepresentationSubmitBackToDetails: () => {
+ setRepresentationSubmit(false);
+ },
+ onRepresentationQuestionnaireBack: () => {
+ setShowQuestionnaireSection(showQuestionnaireSection - 1);
+ setRepresentationSubmit(false);
+ },
+ renderAnchorCrumb,
+ renderLinkCrumb,
+ renderTextCrumb,
+ renderCaseReferenceCrumb,
+ renderMyPortalCrumb,
+ renderMyPortalSectionCrumbs,
+ renderDnsCaseReferenceCrumbs,
+ renderBackCrumb
+ });
+
+ const mappedRouteRendererMaps = buildBreadcrumbRendererMaps({
+ simpleRouteRenderersByPath,
+ simpleLinkTextPairRenderersByPath,
+ simpleMyPortalRouteRenderersByPath,
+ newAppealRouteRenderersByPath,
+ callbackRouteRenderersByPath,
+ stepBackRouteRenderersByPath,
+ representationRouteRenderersByPath,
+ caseDetailRouteRenderersByPath,
+ detailAndAccountRouteRenderersByPath
+ });
+
+ const staticTextCrumbLabel = getStaticTextCrumbLabel(pathname);
- return t("common:breadcrumb-search-results");
- })();
return (
<>
@@ -1031,13 +323,11 @@ const mapDispatchToProps = (dispatch) => {
updateField: (form, field, newValue) =>
dispatch(change(form, field, newValue)),
setRepresentationCapacity: (representationCapacity) => {
- //dispatch(reset("representationForm"));
dispatch(setRepresentationCapacity(representationCapacity));
},
setRepresentationSubmit: (representationSubmit) => {
dispatch(setRepresentationSubmit(representationSubmit));
},
-
setCurrentView: (currentView) => {
dispatch(setCurrentView(currentView));
},
diff --git a/components/case/summary.js b/components/case/summary.js
index 9958de55..660b874d 100644
--- a/components/case/summary.js
+++ b/components/case/summary.js
@@ -45,6 +45,10 @@ import {
showRepsEnded,
getBilingualText
} from "./summary/utils/helpers";
+import {
+ resolveSearchResultsHref,
+ isDnsRoutePath
+} from "../../lib/routing/routeState";
import CaseNoticeBanner from "./caseNoticeBanner";
import WatchModal from "./watchmodal";
@@ -324,52 +328,15 @@ const CaseSummary = (props) => {
const zoom = isNaN(parseFloat(siteCoords.latitude)) ? 7 : 12;
- const buildSearchHref = (router, session) => {
- const { va, adv, ads, key } = router.query;
+ const isDnsRoute = isDnsRoutePath(router.pathname);
- const isDnsRoute =
- router.pathname === "/dns" ||
- router.pathname.startsWith("/dns/") ||
- router.pathname === "/myportal/dns" ||
- router.pathname.startsWith("/myportal/dns/");
-
- const base = session ? "/myportal" : "";
-
- if (!va && !adv && !ads) {
- return {
- pathname: "/myportal"
- };
- }
-
- if (isDnsRoute) {
- return {
- pathname: `${base}/dnsapplications`
- };
- }
-
- if (va === "true") {
- return {
- pathname: "/myportal/viewall",
- query: { key }
- };
- }
-
- if (adv === "true") {
- return {
- pathname: `${base}/advancedsearchresults`,
- query: router.query
- };
- }
-
- if (ads === "true") {
- return {
- pathname: `${base}/addresssearchresults`,
- query: router.query
- };
- }
-
- return { pathname: `${base}/searchresults`, query: router.query };
- };
+ const searchHref = resolveSearchResultsHref({
+ query: router.query,
+ hasSession: Boolean(session),
+ includeViewAll: true,
+ fallbackToMyPortalWhenNoFlags: true,
+ isDnsRoute
+ });
let showDetailsBlock =
showDetails == true ? (
@@ -1043,10 +1010,7 @@ const CaseSummary = (props) => {
{" "}
{t(
diff --git a/components/myportal/viewall.js b/components/myportal/viewall.js
index 6532be5a..0affa007 100644
--- a/components/myportal/viewall.js
+++ b/components/myportal/viewall.js
@@ -523,9 +523,12 @@ const ViewAllResults = (props) => {
? jsonpath({
path:
'$..[?(@ && @.value=="' +
- searchDetailsObj[key].value[0][
- "_pinswg_localplanningauthority_value@OData.Community.Display.V1.FormattedValuee"
- ] +
+ (searchDetailsObj[key][
+ "_pinswg_localplanningauthority_value@OData.Community.Display.V1.FormattedValue"
+ ] ||
+ searchDetailsObj[key]?.value[0][
+ "_pinswg_localplanningauthority_value@OData.Community.Display.V1.FormattedValue"
+ ]) +
'")].value_cy',
json: transLookup,
eval: true
@@ -544,7 +547,7 @@ const ViewAllResults = (props) => {
path:
'$..[?(@ && @.value=="' +
resultItem[
- "_pinswg_localplanningauthority_value@OData.Community.Display.V1.FormattedValuee"
+ "_pinswg_localplanningauthority_value@OData.Community.Display.V1.FormattedValue"
] +
'")].value_cy',
json: transLookup,
diff --git a/i18n.js b/i18n.js
index 09c9872c..5b5abac8 100644
--- a/i18n.js
+++ b/i18n.js
@@ -16,7 +16,7 @@ module.exports = {
"/myportal/contactus": ["newappeal", "account", "common"],
"/error": ["common", "home", "myportal"],
"/furtherdetails": ["common", "home", "myportal"],
- "/myportal": ["myportal", "common", "home", "case"],
+ "/myportal": ["myportal", "common", "home", "case", "auth"],
"/myportal/viewall": ["search", "myportal", "case"],
"/myportal/advancedsearch": ["search", "myportal"],
"/myportal/advancedsearchresults": ["search", "myportal", "case"],
diff --git a/lib/routing/breadcrumbRendererFactories.js b/lib/routing/breadcrumbRendererFactories.js
new file mode 100644
index 00000000..122de2ac
--- /dev/null
+++ b/lib/routing/breadcrumbRendererFactories.js
@@ -0,0 +1,618 @@
+import React from "react";
+
+export const createSimpleRouteRenderers = ({
+ t,
+ renderTextCrumb,
+ renderMyPortalSectionCrumbs
+}) => ({
+ "/myportal": () => renderTextCrumb(t("myportal:page-title")),
+ "/searchresults": () =>
+ renderTextCrumb(t("common:breadcrumb-search-results")),
+ "/advancedsearch": () =>
+ renderTextCrumb(t("common:breadcrumb-advanced-search")),
+ "/addresssearch": () =>
+ renderTextCrumb(t("common:breadcrumb-address-search")),
+ "/contactus": () =>
+ renderTextCrumb(t("common:footer-contact-us-link-label")),
+ "/viewall": () =>
+ renderMyPortalSectionCrumbs(t("common:breadcrumb-your-cases")),
+ "/myportal/dnsapplications": () =>
+ renderMyPortalSectionCrumbs(t("dnsCommon:service-name")),
+ "/dnsapplications": () => renderTextCrumb(t("dnsCommon:service-name"))
+});
+
+export const createSimpleLinkTextPairRenderers = ({
+ t,
+ router,
+ isWelsh,
+ renderLinkCrumb,
+ renderTextCrumb
+}) => ({
+ "/addresssearchresults": () => (
+ <>
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/chwiliadcyfeiriadau`
+ : "/addresssearch",
+ t("common:breadcrumb-address-search")
+ )}
+ {renderTextCrumb(t("common:breadcrumb-address-search-results"))}
+ >
+ ),
+ "/advancedsearchresults": () => (
+ <>
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/advancedsearch`
+ : "/advancedsearch",
+ t("common:breadcrumb-advanced-search")
+ )}
+ {renderTextCrumb(t("common:breadcrumb-advanced-search-results"))}
+ >
+ )
+});
+
+export const createSimpleMyPortalRouteRenderers = ({
+ t,
+ router,
+ isWelsh,
+ myPortalHref,
+ currentViewState,
+ getViewAllLabel,
+ renderAnchorCrumb,
+ renderLinkCrumb,
+ renderTextCrumb,
+ renderMyPortalCrumb,
+ renderMyPortalSectionCrumbs
+}) => ({
+ "/myportal/searchresults": () => (
+ <>
+ {renderAnchorCrumb(myPortalHref, t("common:breadcrumb-my-portal"))}
+ {renderTextCrumb(t("common:breadcrumb-search-results"))}
+ >
+ ),
+ "/myportal/addresssearch": () =>
+ renderMyPortalSectionCrumbs(t("common:breadcrumb-address-search")),
+ "/myportal/advancedsearch": () =>
+ renderMyPortalSectionCrumbs(t("common:breadcrumb-advanced-search")),
+ "/myportal/contactus": () =>
+ renderMyPortalSectionCrumbs(t("common:footer-contact-us-link-label")),
+ "/myportal/viewall": () =>
+ renderMyPortalSectionCrumbs(
+ getViewAllLabel(currentViewState.viewKey || router.query.key)
+ ),
+ "/myportal/advancedsearchresults": () => (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/myportal/advancedsearch`
+ : "/myportal/advancedsearch",
+ t("common:breadcrumb-advanced-search")
+ )}
+ {renderTextCrumb(t("common:breadcrumb-advanced-search-results"))}
+ >
+ ),
+ "/myportal/addresssearchresults": () => (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/fymhorth/chwiliadcyfeiriadau`
+ : "/myportal/addresssearch",
+ t("common:breadcrumb-address-search")
+ )}
+ {renderTextCrumb(t("common:breadcrumb-address-search-results"))}
+ >
+ )
+});
+
+export const createNewAppealRouteRenderers = ({
+ t,
+ myPortalHref,
+ renderLinkCrumb,
+ renderTextCrumb
+}) => ({
+ "/newappeal/selectappeal": () => (
+ <>
+ {renderLinkCrumb(myPortalHref, t("newappeal:parent-page-title"))}
+ {renderLinkCrumb(myPortalHref, t("newappeal:page-title"))}
+ {renderTextCrumb("Select Appeal")}
+ >
+ )
+});
+
+export const createCallbackRouteRenderers = ({
+ t,
+ router,
+ isWelsh,
+ nestedSearchString,
+ currentReference,
+ renderMyPortalCrumb,
+ renderLinkCrumb,
+ renderCaseReferenceCrumb,
+ onBack
+}) => ({
+ "/myportal/case/id/[incident]": () => (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/fymhorth/chwiliadcyfeiriadau`
+ : "/myportal/addresssearch",
+ t("common:breadcrumb-address-search")
+ )}
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/fymhorth/canlyniadaucyfeiriadau?${nestedSearchString}`
+ : `/myportal/addresssearchresults?${nestedSearchString}`,
+ t("common:breadcrumb-address-search-results"),
+ onBack
+ )}
+ {renderCaseReferenceCrumb(currentReference)}
+ >
+ ),
+
+ "/case": () => (
+ <>
+ {renderLinkCrumb(
+ isWelsh
+ ? `${router.locale}/searchresults?q=${router.query.q}&page=${router.query.page}`
+ : `/searchresults?q=${router.query.q}&page=${router.query.page}`,
+ t("common:breadcrumb-search-results"),
+ onBack
+ )}
+ {renderCaseReferenceCrumb(currentReference)}
+ >
+ )
+});
+
+export const createStepBackRouteRenderers = ({
+ t,
+ currentSection,
+ onStepBack,
+ onStepBackWithInlineClass,
+ renderLinkCrumb,
+ renderBackCrumb
+}) => ({
+ "/newappeal/[appealtypes]": () => {
+ if (currentSection <= 1) {
+ return null;
+ }
+
+ if (currentSection === 9999) {
+ return renderLinkCrumb("/", t("common:service-name-breadcrumb"));
+ }
+
+ return renderBackCrumb(
+ onStepBackWithInlineClass,
+ "govuk-breadcrumbs__link-item"
+ );
+ },
+
+ "/myportal/[appealtypes]": () => {
+ if (currentSection <= 1) {
+ return null;
+ }
+
+ if (currentSection === 9999) {
+ return renderLinkCrumb("/", t("common:service-name-breadcrumb"));
+ }
+
+ return renderBackCrumb(onStepBack);
+ }
+});
+
+export const createRepresentationRouteRenderers = ({
+ currentView,
+ hasRepDetails,
+ repDetails,
+ showQuestionnaireSection,
+ onRepresentationSubmitBackFromConfirmation,
+ onRepresentationSubmitBackToDetails,
+ onRepresentationQuestionnaireBack,
+ renderBackCrumb
+}) => ({
+ "/myportal/representation": () => (
+ <>
+ {currentView.representationSubmit === true &&
+ currentView.representationSubmitConfirmation !== true &&
+ renderBackCrumb(onRepresentationSubmitBackFromConfirmation)}
+
+ {currentView.representationSubmit !== true &&
+ hasRepDetails &&
+ repDetails?.representationType !== "Questionnaire" &&
+ renderBackCrumb(onRepresentationSubmitBackToDetails)}
+
+ {currentView.representationSubmit !== true &&
+ hasRepDetails &&
+ repDetails?.representationType === "Questionnaire" &&
+ showQuestionnaireSection > 1 &&
+ renderBackCrumb(onRepresentationQuestionnaireBack)}
+ >
+ )
+});
+
+export const createCaseDetailRouteRenderers = ({
+ t,
+ router,
+ isWelsh,
+ currentReference,
+ nestedSearchString,
+ caseResultsHref,
+ breadcrumbLabel,
+ fallbackSearchTitle,
+ caseReferenceDisplay,
+ getViewAllLabel,
+ renderLinkCrumb,
+ renderCaseReferenceCrumb,
+ renderMyPortalCrumb
+}) => ({
+ "/myportal/case": () => (
+ <>
+ {renderLinkCrumb(
+ isWelsh ? `/${router.locale}/fymhorth` : "/myportal",
+ t("common:breadcrumb-my-portal")
+ )}
+ {renderCaseReferenceCrumb(currentReference)}
+ >
+ ),
+
+ "/case/id/[incident]": () => (
+ <>
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/chwiliadcyfeiriadau`
+ : "/addresssearch",
+ t("common:breadcrumb-address-search")
+ )}
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/canlyniadaucyfeiriadau?${nestedSearchString}`
+ : `/addresssearchresults?${nestedSearchString}`,
+ t("common:breadcrumb-address-search-results")
+ )}
+ {renderCaseReferenceCrumb(currentReference)}
+ >
+ ),
+
+ "/case/[ticketnumber]": () => {
+ const { ads, adv, va, key } = router.query;
+
+ if (ads === "true") {
+ return (
+ <>
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/chwiliadcyfeiriadau`
+ : "/addresssearch",
+ t("common:breadcrumb-address-search")
+ )}
+ {renderLinkCrumb(
+ caseResultsHref,
+ t("common:breadcrumb-address-search-results")
+ )}
+ {renderCaseReferenceCrumb(fallbackSearchTitle)}
+ >
+ );
+ }
+
+ if (adv === "true") {
+ return (
+ <>
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/advancedsearch`
+ : "/advancedsearch",
+ t("common:breadcrumb-advanced-search")
+ )}
+ {renderLinkCrumb(
+ caseResultsHref,
+ t("common:breadcrumb-advanced-search-results")
+ )}
+ {renderCaseReferenceCrumb(fallbackSearchTitle)}
+ >
+ );
+ }
+
+ if (va === "true") {
+ return (
+ <>
+ {renderLinkCrumb("/myportal/viewall", getViewAllLabel(key))}
+ {renderCaseReferenceCrumb(fallbackSearchTitle)}
+ >
+ );
+ }
+
+ return (
+ <>
+ {renderLinkCrumb(caseResultsHref, breadcrumbLabel)}
+ {renderCaseReferenceCrumb(fallbackSearchTitle)}
+ >
+ );
+ },
+
+ "/myportal/case/[ticketnumber]": () => {
+ const { ads, adv, va, key } = router.query;
+
+ if (ads === "true") {
+ return (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/fymhorth/chwiliadcyfeiriadau`
+ : "/myportal/addresssearch",
+ t("common:breadcrumb-address-search")
+ )}
+ {renderLinkCrumb(
+ caseResultsHref,
+ t("common:breadcrumb-address-search-results")
+ )}
+ {renderCaseReferenceCrumb(caseReferenceDisplay)}
+ >
+ );
+ }
+
+ if (adv === "true") {
+ return (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/myportal/advancedsearch`
+ : "/myportal/advancedsearch",
+ t("common:breadcrumb-advanced-search")
+ )}
+ {renderLinkCrumb(
+ caseResultsHref,
+ t("common:breadcrumb-advanced-search-results")
+ )}
+ {renderCaseReferenceCrumb(caseReferenceDisplay)}
+ >
+ );
+ }
+
+ if (va === "true") {
+ return (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb("/myportal/viewall", getViewAllLabel(key))}
+ {renderCaseReferenceCrumb(caseReferenceDisplay)}
+ >
+ );
+ }
+
+ return (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb(caseResultsHref, breadcrumbLabel)}
+ {renderCaseReferenceCrumb(caseReferenceDisplay)}
+ >
+ );
+ }
+});
+
+export const createDetailAndAccountRouteRenderers = ({
+ t,
+ router,
+ isWelsh,
+ cyMyPortalHref,
+ currentReference,
+ caseReferenceDisplay,
+ renderMyPortalCrumb,
+ renderLinkCrumb,
+ renderCaseReferenceCrumb,
+ renderDnsCaseReferenceCrumbs
+}) => ({
+ "/myportal/dnsdetails": () => (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb(
+ "/myportal/dnsapplications",
+ t("dnsCommon:service-name")
+ )}
+ {renderCaseReferenceCrumb(currentReference)}
+ >
+ ),
+
+ "/myportal/dns/[developmentName]": () => (
+ <>
+ {renderMyPortalCrumb()}
+ {renderLinkCrumb(
+ "/myportal/dnsapplications",
+ t("dnsCommon:service-name")
+ )}
+ {renderCaseReferenceCrumb(caseReferenceDisplay)}
+ >
+ ),
+
+ "/dnsdetails": () => renderDnsCaseReferenceCrumbs(currentReference),
+
+ "/dns/[developmentName]": () =>
+ renderDnsCaseReferenceCrumbs(caseReferenceDisplay),
+
+ "/dns/application-view": () => (
+ <>
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/dns/applications`
+ : "/dns/applications",
+ t("dnsApplicationView:page-parent-title")
+ )}
+ {renderLinkCrumb(
+ isWelsh
+ ? `/${router.locale}/dnsapplications`
+ : "/dnsapplications",
+ t("dnsCommon:service-name")
+ )}
+ {renderCaseReferenceCrumb("TWA - Morlais Demonstration Zone")}
+ >
+ ),
+
+ "/account/personaldetails": () => (
+ <>
+ {renderLinkCrumb(cyMyPortalHref, t("common:breadcrumb-my-portal"))}
+
+ {t("account:account-title")}
+
+ >
+ ),
+
+ "/account/changepassword": () => (
+ <>
+ {renderLinkCrumb(cyMyPortalHref, t("common:breadcrumb-my-portal"))}
+
+ Update your password
+
+ >
+ )
+});
+
+export const createMappedRouteRendererGroups = ({
+ t,
+ router,
+ isWelsh,
+ myPortalHref,
+ cyMyPortalHref,
+ currentViewState,
+ getViewAllLabel,
+ caseResultsHref,
+ breadcrumbLabel,
+ fallbackSearchTitle,
+ currentReference,
+ nestedSearchString,
+ caseReferenceDisplay,
+ onBack,
+ currentSection,
+ onStepBack,
+ onStepBackWithInlineClass,
+ currentView,
+ hasRepDetails,
+ repDetails,
+ showQuestionnaireSection,
+ onRepresentationSubmitBackFromConfirmation,
+ onRepresentationSubmitBackToDetails,
+ onRepresentationQuestionnaireBack,
+ renderAnchorCrumb,
+ renderLinkCrumb,
+ renderTextCrumb,
+ renderCaseReferenceCrumb,
+ renderMyPortalCrumb,
+ renderMyPortalSectionCrumbs,
+ renderDnsCaseReferenceCrumbs,
+ renderBackCrumb
+}) => {
+ const simpleRouteRenderersByPath = createSimpleRouteRenderers({
+ t,
+ renderTextCrumb,
+ renderMyPortalSectionCrumbs
+ });
+
+ const simpleLinkTextPairRenderersByPath = createSimpleLinkTextPairRenderers(
+ {
+ t,
+ router,
+ isWelsh,
+ renderLinkCrumb,
+ renderTextCrumb
+ }
+ );
+
+ const simpleMyPortalRouteRenderersByPath =
+ createSimpleMyPortalRouteRenderers({
+ t,
+ router,
+ isWelsh,
+ myPortalHref,
+ currentViewState,
+ getViewAllLabel,
+ renderAnchorCrumb,
+ renderLinkCrumb,
+ renderTextCrumb,
+ renderMyPortalCrumb,
+ renderMyPortalSectionCrumbs
+ });
+
+ const newAppealRouteRenderersByPath = createNewAppealRouteRenderers({
+ t,
+ myPortalHref,
+ renderLinkCrumb,
+ renderTextCrumb
+ });
+
+ const callbackRouteRenderersByPath = createCallbackRouteRenderers({
+ t,
+ router,
+ isWelsh,
+ nestedSearchString,
+ currentReference,
+ renderMyPortalCrumb,
+ renderLinkCrumb,
+ renderCaseReferenceCrumb,
+ onBack
+ });
+
+ const stepBackRouteRenderersByPath = createStepBackRouteRenderers({
+ t,
+ currentSection,
+ onStepBack,
+ onStepBackWithInlineClass,
+ renderLinkCrumb,
+ renderBackCrumb
+ });
+
+ const representationRouteRenderersByPath =
+ createRepresentationRouteRenderers({
+ currentView,
+ hasRepDetails,
+ repDetails,
+ showQuestionnaireSection,
+ onRepresentationSubmitBackFromConfirmation,
+ onRepresentationSubmitBackToDetails,
+ onRepresentationQuestionnaireBack,
+ renderBackCrumb
+ });
+
+ const caseDetailRouteRenderersByPath = createCaseDetailRouteRenderers({
+ t,
+ router,
+ isWelsh,
+ currentReference,
+ nestedSearchString,
+ caseResultsHref,
+ breadcrumbLabel,
+ fallbackSearchTitle,
+ caseReferenceDisplay,
+ getViewAllLabel,
+ renderLinkCrumb,
+ renderCaseReferenceCrumb,
+ renderMyPortalCrumb
+ });
+
+ const detailAndAccountRouteRenderersByPath =
+ createDetailAndAccountRouteRenderers({
+ t,
+ router,
+ isWelsh,
+ cyMyPortalHref,
+ currentReference,
+ caseReferenceDisplay,
+ renderMyPortalCrumb,
+ renderLinkCrumb,
+ renderCaseReferenceCrumb,
+ renderDnsCaseReferenceCrumbs
+ });
+
+ return {
+ simpleRouteRenderersByPath,
+ simpleLinkTextPairRenderersByPath,
+ simpleMyPortalRouteRenderersByPath,
+ newAppealRouteRenderersByPath,
+ callbackRouteRenderersByPath,
+ stepBackRouteRenderersByPath,
+ representationRouteRenderersByPath,
+ caseDetailRouteRenderersByPath,
+ detailAndAccountRouteRenderersByPath
+ };
+};
diff --git a/lib/routing/breadcrumbRouteMaps.js b/lib/routing/breadcrumbRouteMaps.js
new file mode 100644
index 00000000..81c11364
--- /dev/null
+++ b/lib/routing/breadcrumbRouteMaps.js
@@ -0,0 +1,47 @@
+const getMappedRendererByPath = (path, routeRendererMap) => {
+ if (!routeRendererMap || typeof routeRendererMap !== "object") {
+ return null;
+ }
+
+ const routeRenderer = routeRendererMap[path];
+ return typeof routeRenderer === "function" ? routeRenderer : null;
+};
+
+const resolveMappedRouteRenderer = (path, routeRendererMaps = []) => {
+ for (const routeRendererMap of routeRendererMaps) {
+ const routeRenderer = getMappedRendererByPath(path, routeRendererMap);
+ if (routeRenderer) {
+ return routeRenderer;
+ }
+ }
+
+ return null;
+};
+
+const buildBreadcrumbRendererMaps = ({
+ simpleRouteRenderersByPath,
+ simpleLinkTextPairRenderersByPath,
+ simpleMyPortalRouteRenderersByPath,
+ newAppealRouteRenderersByPath,
+ callbackRouteRenderersByPath,
+ stepBackRouteRenderersByPath,
+ representationRouteRenderersByPath,
+ caseDetailRouteRenderersByPath,
+ detailAndAccountRouteRenderersByPath
+}) => [
+ simpleRouteRenderersByPath,
+ simpleLinkTextPairRenderersByPath,
+ simpleMyPortalRouteRenderersByPath,
+ newAppealRouteRenderersByPath,
+ callbackRouteRenderersByPath,
+ stepBackRouteRenderersByPath,
+ representationRouteRenderersByPath,
+ caseDetailRouteRenderersByPath,
+ detailAndAccountRouteRenderersByPath
+];
+
+module.exports = {
+ buildBreadcrumbRendererMaps,
+ getMappedRendererByPath,
+ resolveMappedRouteRenderer
+};
diff --git a/lib/routing/routeState.js b/lib/routing/routeState.js
new file mode 100644
index 00000000..5d334ba0
--- /dev/null
+++ b/lib/routing/routeState.js
@@ -0,0 +1,133 @@
+export const normalizeRouteStateQuery = (query = {}) => {
+ const viewAll = query?.va === "true";
+ const advanced = query?.adv === "true";
+ const address = query?.ads === "true";
+
+ return {
+ viewAll,
+ advanced,
+ address,
+ key: query?.key
+ };
+};
+
+export const resolveSearchResultsHref = ({
+ query = {},
+ hasSession = false,
+ includeViewAll = true,
+ fallbackToMyPortalWhenNoFlags = false,
+ isDnsRoute = false
+} = {}) => {
+ const routeState = normalizeRouteStateQuery(query);
+ const { viewAll, advanced, address, key } = routeState;
+ const base = hasSession ? "/myportal" : "";
+ const hasSearchQuery =
+ typeof query?.q === "string" && query.q.trim().length > 0;
+
+ if (fallbackToMyPortalWhenNoFlags && !viewAll && !advanced && !address) {
+ if (hasSearchQuery) {
+ return {
+ pathname: `${base}/searchresults`,
+ query
+ };
+ }
+ return {
+ pathname: "/myportal"
+ };
+ }
+
+ if (isDnsRoute) {
+ return {
+ pathname: `${base}/dnsapplications`
+ };
+ }
+
+ if (includeViewAll && viewAll) {
+ return {
+ pathname: "/myportal/viewall",
+ query: { key }
+ };
+ }
+
+ if (advanced) {
+ return {
+ pathname: `${base}/advancedsearchresults`,
+ query
+ };
+ }
+
+ if (address) {
+ return {
+ pathname: `${base}/addresssearchresults`,
+ query
+ };
+ }
+
+ return {
+ pathname: `${base}/searchresults`,
+ query
+ };
+};
+
+export const resolveSearchBreadcrumbLabel = ({
+ query = {},
+ getViewAllLabel = () => null,
+ advancedLabel = "",
+ addressLabel = "",
+ defaultLabel = ""
+} = {}) => {
+ const routeState = normalizeRouteStateQuery(query);
+ const { viewAll, advanced, address, key } = routeState;
+
+ if (viewAll) {
+ return getViewAllLabel(key);
+ }
+
+ const keyedLabel = getViewAllLabel(key);
+ if (keyedLabel) return keyedLabel;
+
+ if (advanced) return advancedLabel;
+ if (address) return addressLabel;
+
+ return defaultLabel;
+};
+
+export const isDnsRoutePath = (pathname = "") => {
+ return (
+ pathname === "/dns" ||
+ pathname.startsWith("/dns/") ||
+ pathname === "/myportal/dns" ||
+ pathname.startsWith("/myportal/dns/")
+ );
+};
+
+export const resolveCaseBreadcrumbState = ({
+ query = {},
+ hasSession = false,
+ getViewAllLabel = () => null,
+ advancedLabel = "",
+ addressLabel = "",
+ defaultLabel = ""
+} = {}) => {
+ return {
+ breadcrumbHref: resolveSearchResultsHref({
+ query,
+ hasSession,
+ includeViewAll: true,
+ fallbackToMyPortalWhenNoFlags: true
+ }),
+ caseResultsHref: resolveSearchResultsHref({
+ query,
+ hasSession,
+ includeViewAll: false,
+ fallbackToMyPortalWhenNoFlags: false
+ }),
+ breadcrumbLabel: resolveSearchBreadcrumbLabel({
+ query,
+ getViewAllLabel,
+ advancedLabel,
+ addressLabel,
+ defaultLabel
+ })
+ };
+};
diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md
index 28f12758..a2de7b0a 100644
--- a/memory-bank/change-log.md
+++ b/memory-bank/change-log.md
@@ -18,6 +18,900 @@ Follow-ups:
---
+### CL-22541: breadcrumb/back-link route-state helper extraction (`va/adv/ads/key`)
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/routeState.js`, `components/breadcrumbs.js`, `components/case/summary.js`, `tests/phase22/{route-state-helper.test.cjs,index.test.cjs}`
+type: change
+rationale: Action Priority 3 refactor backlog item by extracting duplicated route-state decision logic (`va/adv/ads/key`) into a shared pure helper and reusing it from breadcrumb and case-summary back-link flows to reduce drift/regression risk.
+impact: Refactor-only decision-layer consolidation for search/case/myportal navigation; no auth/session/CSP/API contract changes; EN/CY behavior intended unchanged as helper only resolves route targets and preserves existing query forwarding behavior.
+status: completed
+
+Summary:
+
+- Created branch from `origin/SIPS-Development`: `22541-breadcrumb-route-state-helper`.
+- Added `lib/routing/routeState.js` with:
+ - `normalizeRouteStateQuery(query)`
+ - `resolveSearchResultsHref(options)`
+- Updated `components/breadcrumbs.js` to consume the shared helper for:
+ - breadcrumb href resolution (`/myportal/viewall`, advanced/address/default results)
+ - case-results breadcrumb target on `/case/[ticketnumber]`
+ - preserving existing label selection behavior via normalized flags.
+- Updated `components/case/summary.js` to replace local `buildSearchHref` with shared helper output (`searchHref`), including DNS-route override and no-flag fallback behavior.
+- Added table-driven unit coverage in `tests/phase22/route-state-helper.test.cjs` and registered it in `tests/phase22/index.test.cjs`.
+
+Validation:
+
+- `npx eslint lib/routing/routeState.js components/breadcrumbs.js components/case/summary.js tests/phase22/route-state-helper.test.cjs tests/phase22/index.test.cjs` -> pass with 1 pre-existing warning in `components/case/summary.js` (`react-hooks/exhaustive-deps`).
+- `node tests/phase22/route-state-helper.test.cjs` -> pass (5/5).
+- `node tests/phase22/index.test.cjs` -> fails in existing auth suite (`tests/phase22/auth-redirect-safety.test.cjs`) with `ReferenceError: resolveLocale is not defined` in `pages/api/auth/[...nextauth].js`; unrelated to route-state changes.
+
+Follow-ups:
+
+- If needed, triage/fix the existing phase22 auth redirect test harness failure separately to restore full combined suite execution.
+- Expand helper adoption in future slices to other navigation decision call sites if additional duplication emerges.
+
+### CL-22541-B: breadcrumb route-state helper expansion (label + DNS path helper)
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/routeState.js`, `components/breadcrumbs.js`, `components/case/summary.js`, `tests/phase22/route-state-helper.test.cjs`
+type: change
+rationale: Continue Priority 3 breadcrumb refactor in a second bounded commit by extracting remaining repeated breadcrumb label and DNS-route-path decision logic into shared pure helpers.
+impact: Refactor-only extraction for navigation decision logic; no auth/session/CSP/API changes; no intended EN/CY behavior change beyond internal logic centralization.
+status: completed
+
+Summary:
+
+- Extended `lib/routing/routeState.js` with:
+ - `resolveSearchBreadcrumbLabel(...)`
+ - `isDnsRoutePath(pathname)`
+- Updated `components/breadcrumbs.js` to use `resolveSearchBreadcrumbLabel(...)` and removed inline label decision IIFE.
+- Updated `components/case/summary.js` to use `isDnsRoutePath(router.pathname)` and removed inline DNS path checks.
+- Expanded `tests/phase22/route-state-helper.test.cjs` with coverage for:
+ - breadcrumb label resolution matrix
+ - DNS path detection helper
+
+Validation:
+
+- `npx eslint lib/routing/routeState.js components/breadcrumbs.js components/case/summary.js tests/phase22/route-state-helper.test.cjs` -> pass with 1 pre-existing warning in `components/case/summary.js` (`react-hooks/exhaustive-deps`).
+- `node tests/phase22/route-state-helper.test.cjs` -> pass (7/7).
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next breadcrumbs slice can target incremental decomposition of large pathname branch rendering blocks in `components/breadcrumbs.js` into grouped render helpers with behavior parity.
+
+### CL-22541-C: breadcrumb case-state bundling helper (single-call resolver)
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/routeState.js`, `components/breadcrumbs.js`, `tests/phase22/route-state-helper.test.cjs`
+type: change
+rationale: Continue Priority 3 breadcrumbs refactor by bundling repeatedly paired breadcrumb href/label resolution into one pure helper so component call sites stay thinner and less drift-prone.
+impact: Refactor-only centralization of existing route-state composition logic; no intended auth/session/API/i18n behavior change.
+status: completed
+
+Summary:
+
+- Added `resolveCaseBreadcrumbState(...)` in `lib/routing/routeState.js` to return:
+ - `breadcrumbHref`
+ - `caseResultsHref`
+ - `breadcrumbLabel`
+- Updated `components/breadcrumbs.js` to replace three separate helper invocations with one resolver call.
+- Extended `tests/phase22/route-state-helper.test.cjs` with bundled case-state resolver coverage.
+
+Validation:
+
+- `npx eslint lib/routing/routeState.js components/breadcrumbs.js tests/phase22/route-state-helper.test.cjs` -> pass.
+- `node tests/phase22/route-state-helper.test.cjs` -> pass (8/8).
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next bounded slice can extract grouped pathname render blocks from `components/breadcrumbs.js` (data-driven map or small render helpers) while preserving route parity.
+
+### CL-22541-D: breadcrumbs shared my-portal crumb helper extraction
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Continue bounded breadcrumbs decomposition by extracting repeated my-portal breadcrumb `` markup into one local render helper to reduce duplication and keep follow-on slices safer.
+impact: Refactor-only JSX deduplication in breadcrumb rendering; no intended route, auth/session, API, EN/CY, or accessibility behavior change.
+status: completed
+
+Summary:
+
+- Added local `renderMyPortalCrumb()` helper in `components/breadcrumbs.js`.
+- Replaced repeated identical my-portal crumb JSX in multiple `/myportal/*` route branches with helper calls.
+- Kept special-case crumbs untouched where label/href differs (e.g. `newappeal` parent-title crumb, `cyMyPortalHref` account branches).
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next bounded slice can target additional repeated breadcrumb list-item patterns (e.g., repeated case-reference tail item blocks) with same behavior parity approach.
+
+### CL-22541-E: breadcrumbs case-reference crumb helper extraction
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Continue bounded breadcrumbs decomposition by extracting repeated case-reference tail breadcrumb markup into one local helper and replacing repeated inline blocks.
+impact: Refactor-only JSX deduplication in breadcrumb rendering; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added local `renderCaseReferenceCrumb(referenceValue)` helper in `components/breadcrumbs.js`.
+- Replaced repeated `{t("common:breadcrumb-case-reference")}: ...` list-item blocks across case/dns route branches with helper calls.
+- Corrected initial over-wrap by using helper calls directly (so generated `` is not nested in another ``).
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next bounded slice can target remaining repeated linked breadcrumb list-item patterns (e.g., recurring address-search result link pairs) via small local render helpers.
+
+### CL-22541-F: breadcrumbs multi-pattern helper bundle (text + link crumbs)
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Deliver a slightly larger bounded slice by extracting and reusing multiple breadcrumb rendering patterns (plain text crumb and linked crumb) across several route branches.
+impact: Refactor-only JSX deduplication and consistency improvement in breadcrumb rendering; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added reusable helpers in `components/breadcrumbs.js`:
+ - `renderTextCrumb(label)`
+ - `renderLinkCrumb(href, label, onClick)`
+- Replaced multiple repeated inline breadcrumb blocks across search/address/myportal/dns branches with helper calls.
+- Included onClick-capable link helper usage in the `/myportal/case/id/[incident]` address-results breadcrumb branch to preserve `router.back()` behavior.
+- Combined with existing `renderMyPortalCrumb` and `renderCaseReferenceCrumb` helpers for more consistent branch composition.
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next slice recommendation: extract remaining repeated inline link crumbs for case/DNS branches that still hardcode `` (e.g., `/myportal/case`, `/myportal/case/[ticketnumber]`, `/dnsdetails`, `/dns/[developmentName]`) to complete link-pattern consolidation.
+
+### CL-22541-G: breadcrumbs inline Link-crumb consolidation bundle
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Continue the larger bounded breadcrumb refactor cadence by replacing the next batch of repeated inline `` crumbs with the shared link helper.
+impact: Refactor-only JSX deduplication and consistency improvement in breadcrumb rendering; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Reused `renderLinkCrumb(href, label, onClick)` for another broad set of previously inline Link crumbs, including:
+ - service-name crumb in the shared shell
+ - new appeal select-appeal parent/title crumbs
+ - case and ticketnumber branches (`/myportal/case`, `/case/[ticketnumber]`, `/case/id/[incident]`, `/myportal/case/[ticketnumber]`)
+ - additional myportal/dns/account branches (`/myportal/dns/[developmentName]`, `/dnsdetails`, `/dns/[developmentName]`, `/dns/application-view`, account crumbs)
+- Kept special behavior intact where needed (e.g., existing callback-based crumbs that already use helper `onClick`).
+- Increased consistency of crumb rendering style by routing most link crumbs through one helper.
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next slice recommendation: target remaining anchor/back-link duplication (`` and occasional ``) by introducing a bounded helper for action/back crumbs while preserving existing non-Link semantics.
+
+### CL-22541-H: breadcrumbs anchor/back-link helper consolidation bundle
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Continue the larger bounded breadcrumbs refactor cadence by extracting repeated anchor/back-link crumb markup into one local helper while preserving non-Link semantics.
+impact: Refactor-only JSX deduplication and consistency improvement in breadcrumb rendering; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `renderAnchorCrumb(href, label, onClick, listItemClass)` in `components/breadcrumbs.js` for repeated `` breadcrumb actions.
+- Replaced repeated anchor/back-link crumb blocks in:
+ - `/myportal/searchresults` my-portal anchor crumb
+ - `/newappeal/[appealtypes]` and `/myportal/[appealtypes]` step-back crumbs
+ - `/myportal/representation` back-chevron action crumbs (all three state branches)
+- Preserved original click handlers, href values, and class semantics (`govuk-breadcrumbs__link-item`, `backChevron`) to keep behavior parity.
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next slice recommendation: consolidate remaining one-off inline breadcrumb `` blocks still present in edge branches (e.g., `/newappeal/[appealtypes]`, `/myportal/[appealtypes]`, `/case`) and consider introducing an optional `renderBackCrumb(...)` semantic wrapper for readability.
+
+### CL-22541-I: breadcrumbs edge branch consolidation + semantic back-crumb wrapper
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Continue the same bounded breadcrumbs refactor cadence by consolidating remaining edge inline Link/back crumbs and introducing a semantic back-crumb wrapper for readability.
+impact: Refactor-only JSX deduplication/readability improvement; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added semantic helper `renderBackCrumb(onClick, listItemClass?)` that delegates to `renderAnchorCrumb(...)` with existing back-link text.
+- Replaced remaining one-off inline edge crumbs in:
+ - `/newappeal/[appealtypes]` and `/myportal/[appealtypes]` (`currentSection === 9999` service-name link and back-link branch)
+ - `/case` search-results link crumb (including existing `router.back()` callback)
+ - `/myportal/representation` back-link branches now use `renderBackCrumb(...)`.
+- Preserved existing href targets, callback behavior, and class semantics (including special `govuk-breadcrumbs__link-item` usage).
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next slice recommendation: consider extracting a small, route-keyed breadcrumb branch map/helper grouping in `components/breadcrumbs.js` (purely structural) to reduce the long inline conditional chain while keeping one-slice behavior parity.
+
+### CL-22541-J: breadcrumbs static text-only route crumb consolidation
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Continue the same bounded refactor cadence by consolidating repeated static text-only breadcrumb route branches into one route-label resolver.
+impact: Refactor-only structural deduplication/readability improvement; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `getStaticTextCrumbLabel(path)` in `components/breadcrumbs.js` to centralize static text-only crumb labels by pathname.
+- Replaced repeated single-line static crumb branches with one generic render path:
+ - `renderTextCrumb(getStaticTextCrumbLabel(pathname))` when mapping returns a label.
+- Consolidated static branch handling for routes including:
+ - account register
+ - DNS index/help/contact/applications process pages
+ - cookies/privacy/accessibility/static auth routes.
+- Preserved route labels (including translation keys and legacy hardcoded labels where already present).
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next slice recommendation: extract a small grouped helper for recurring two-crumb route pairs (e.g., my-portal + section label, dns list + case reference) to reduce the remaining conditional chain while preserving explicit route behavior.
+
+### CL-22541-K: breadcrumbs grouped two-crumb helper extraction
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Continue the same bounded refactor cadence by extracting recurring two-crumb patterns into small grouped helpers while preserving explicit route branches.
+impact: Refactor-only readability/deduplication improvement; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added grouped helper `renderMyPortalSectionCrumbs(sectionLabel)` for recurring my-portal + section text pairs.
+- Added grouped helper `renderDnsCaseReferenceCrumbs(referenceValue)` for recurring DNS applications link + case-reference pairs.
+- Replaced duplicated route branches with grouped helper usage for:
+ - `/myportal/addresssearch`, `/myportal/advancedsearch`, `/myportal/contactus`
+ - `/dnsdetails`, `/dns/[developmentName]`
+- Preserved existing href behavior (including EN/CY DNS applications link), labels, and case-reference rendering.
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next slice recommendation: extract a small route-to-renderer map for static single-branch crumbs (where branch has no bespoke side-effects) so the main JSX conditional chain shrinks further without altering explicit behavior in dynamic/sensitive branches.
+
+### CL-22541-L: breadcrumbs simple-route renderer map introduction
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`
+type: change
+rationale: Continue bounded readability refactor by introducing a small route-to-renderer map for simple static branches, reducing conditional-chain length while preserving explicit dynamic branches.
+impact: Refactor-only structural deduplication/readability improvement; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `getSimpleRouteRenderer(path)` in `components/breadcrumbs.js` for simple routes with no bespoke side effects.
+- Introduced a single invocation block that resolves and renders mapped simple routes when present.
+- Migrated simple route branches into the map, including:
+ - `/myportal`, `/searchresults`, `/advancedsearch`, `/addresssearch`, `/contactus`
+ - `/viewall`, `/myportal/dnsapplications`
+- Preserved existing label text, helper usage, and rendered crumb sequence semantics.
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next slice recommendation: extract a second tiny map for simple "link + text" pair routes (no callbacks, no side effects) to further shrink the conditional chain while keeping callback/dynamic branches explicit.
+
+### CL-22541-M: breadcrumbs simple link+text pair map + focused structure coverage
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`, `tests/phase22/{breadcrumbs-route-map-structure,index}.test.cjs`
+type: change
+rationale: Execute the next bounded slice by extracting simple link+text pair route branches into a tiny renderer map and adding focused regression checks that mapped and dynamic callback branches remain in the intended structure.
+impact: Refactor-only structural deduplication/readability improvement plus focused test coverage; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `getSimpleLinkTextPairRenderer(path)` in `components/breadcrumbs.js` for simple no-callback "link + text" pair routes.
+- Migrated these routes into the new pair map:
+ - `/addresssearchresults`
+ - `/advancedsearchresults`
+- Kept dynamic/callback-driven branches explicit and untouched (e.g., `/myportal/case/id/[incident]` with `router.back()`).
+- Added focused phase22 suite `tests/phase22/breadcrumbs-route-map-structure.test.cjs` asserting:
+ - mapped simple pair routes exist in the new map
+ - explicit dynamic callback branch remains present.
+- Registered the new suite in `tests/phase22/index.test.cjs`.
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js tests/phase22/index.test.cjs tests/phase22/breadcrumbs-route-map-structure.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite, including new breadcrumbs-route-map tests 2/2).
+
+Follow-ups:
+
+- Next slice recommendation: introduce a tiny local `renderMappedRoute(path, resolver)` helper to remove repeated inline IIFE map invocations, then optionally merge simple maps under one top-level resolver while preserving explicit ordering and side-effect-free constraints.
+
+### CL-22541-N: breadcrumbs unified mapped-route resolver + widened structural guardrails
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`, `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+type: change
+rationale: Execute the requested wider next slice by removing duplicated mapped-route invocation patterns, introducing a single top-level mapped resolver with explicit precedence, and migrating an additional safe my-portal route group into map-driven rendering while preserving explicit dynamic/callback branches.
+impact: Refactor-only structural consolidation/readability improvement with widened mapped-route coverage and stronger structure guardrail tests; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `getSimpleMyPortalRouteRenderer(path)` in `components/breadcrumbs.js` and migrated additional safe map-friendly routes:
+ - `/myportal/searchresults`
+ - `/myportal/addresssearch`
+ - `/myportal/advancedsearch`
+ - `/myportal/contactus`
+ - `/myportal/viewall`
+- Added `resolveMappedRouteRenderer(path)` to unify map precedence under one resolver.
+- Added `renderMappedRoute(path, resolver?)` helper and replaced duplicated inline map IIFE invocations with a single call:
+ - `{renderMappedRoute(pathname)}`
+- Kept dynamic/stateful callback branches explicit and unchanged, especially `/myportal/case/id/[incident]` (`router.back()`) and other callback/state driven branches.
+- Expanded `tests/phase22/breadcrumbs-route-map-structure.test.cjs` to assert:
+ - new my-portal route-map entries are present
+ - unified resolver/helper signatures are present
+ - single mapped-route invocation is used in JSX
+ - dynamic callback branch remains explicit.
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite; breadcrumbs-route-map tests 3/3).
+
+Follow-ups:
+
+- Next wider slice candidate: move a second safe myportal results pair group (`/myportal/advancedsearchresults`, `/myportal/addresssearchresults`) into the unified mapped resolver where callback-free, then consider splitting resolver maps into a dedicated local constant section for improved scanability.
+
+### CL-22541-O: breadcrumbs myportal results-pair migration into unified mapped resolver
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`, `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+type: change
+rationale: Execute the agreed next slice by migrating the remaining callback-free myportal results-pair branches into the unified mapped resolver so more safe branches are rendered through one consistent mapping flow.
+impact: Refactor-only structural consolidation/readability improvement; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Extended `getSimpleMyPortalRouteRenderer(path)` to include:
+ - `/myportal/advancedsearchresults`
+ - `/myportal/addresssearchresults`
+- Removed now-redundant explicit conditional branches for those two routes from the main JSX chain.
+- Kept callback/stateful branches explicit and unchanged (notably `/myportal/case/id/[incident]` with `router.back()`).
+- Expanded `tests/phase22/breadcrumbs-route-map-structure.test.cjs` guardrails to assert:
+ - both myportal results-pair routes are mapped
+ - legacy explicit `isPath(...)` branches for those routes are no longer present
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite; breadcrumbs-route-map tests 3/3).
+
+Follow-ups:
+
+- Next wider slice candidate: split map resolver sections into clearly named grouped constants (simple/public/myportal) near one composition point, then optionally move map-only route renderers to a local pure helper module if continued growth impacts scanability.
+
+### CL-22541-P: breadcrumbs grouped route-map constants + unified map composition point
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`, `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+type: change
+rationale: Execute the next wider slice by replacing per-group resolver functions with grouped route-map constants and a single composition array to further improve scanability while preserving route precedence semantics.
+impact: Refactor-only structural readability improvement; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Replaced function-based map accessors with grouped route-map constants in `components/breadcrumbs.js`:
+ - `simpleRouteRenderersByPath`
+ - `simpleLinkTextPairRenderersByPath`
+ - `simpleMyPortalRouteRenderersByPath`
+- Added unified map composition point:
+ - `mappedRouteRendererMaps` (ordered array preserving precedence)
+ - `getMappedRendererByPath(path, routeRendererMap)` helper
+- Updated `resolveMappedRouteRenderer(path)` to iterate the composed map list while retaining previous resolution order and behavior.
+- Kept dynamic/callback branches explicit and unchanged outside map-driven paths.
+- Expanded structure tests to assert grouped map composition contracts (`mappedRouteRendererMaps`, `getMappedRendererByPath`) in addition to existing resolver and invocation guardrails.
+
+Validation:
+
+- `npx eslint components/breadcrumbs.js tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite; breadcrumbs-route-map tests 3/3).
+
+Follow-ups:
+
+- Next slice option A (larger): extract map constants + resolver helpers into a local `lib/routing/breadcrumbRouteMaps.js` pure module with focused tests, keeping callback/stateful branch rendering in component.
+- Next slice option B (smaller): expand structure tests to assert explicit map ordering invariants so future reordering regressions are caught early.
+
+### CL-22541-Q: breadcrumbs route-map helper module extraction + map-order invariants
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/breadcrumbRouteMaps.js`, `components/breadcrumbs.js`, `tests/phase22/{breadcrumb-route-maps-helper,breadcrumbs-route-map-structure,index}.test.cjs`
+type: change
+rationale: Execute both queued follow-up slices together by extracting map composition/resolution logic into a pure routing helper module and adding stronger structure + pure-helper guardrails for map-order precedence.
+impact: Refactor-only structural modularization and test hardening; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added new pure helper module `lib/routing/breadcrumbRouteMaps.js`:
+ - `buildBreadcrumbRendererMaps(...)`
+ - `getMappedRendererByPath(...)`
+ - `resolveMappedRouteRenderer(...)`
+- Updated `components/breadcrumbs.js` to import/use shared helper logic:
+ - map composition now via `buildBreadcrumbRendererMaps(...)`
+ - resolver now delegates to `resolveMappedRouteRendererByMaps(...)`
+ - dynamic/callback-sensitive explicit branches remain unchanged.
+- Added focused pure-helper tests in `tests/phase22/breadcrumb-route-maps-helper.test.cjs` for:
+ - map build order
+ - map lookup null/renderer behavior
+ - resolver precedence behavior
+- Expanded `tests/phase22/breadcrumbs-route-map-structure.test.cjs` to assert:
+ - component imports shared breadcrumb route-map helper module
+ - resolver delegation uses helper module
+ - map builder call preserves explicit ordering (simple -> link-text -> myportal)
+- Updated `tests/phase22/index.test.cjs` to include the new helper suite.
+
+Validation:
+
+- `npx eslint lib/routing/breadcrumbRouteMaps.js components/breadcrumbs.js tests/phase22/breadcrumb-route-maps-helper.test.cjs tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite; breadcrumb-route-maps-helper 3/3; breadcrumbs-route-map 3/3).
+
+Follow-ups:
+
+- Next slice option A (larger): move per-path map object declarations out of `components/breadcrumbs.js` into module-level pure factories (still injected with `t/router/currentView`) to further shrink component body.
+- Next slice option B (smaller): add one focused parity test asserting mapped resolver fallback returns `null` in component rendering path for non-mapped routes (ensures no accidental render noise).
+
+### CL-22541-R: breadcrumbs mapped-group factory composition + unmapped-route null parity guard
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/breadcrumbRendererFactories.js`, `components/breadcrumbs.js`, `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+type: change
+rationale: Execute both approved next slices in one commit by centralizing deterministic mapped route-group composition into a factory-level helper and adding explicit structure guard coverage for null fallback behavior on unmapped routes.
+impact: Refactor-only structural consolidation and test hardening; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `createMappedRouteRendererGroups(...)` in `lib/routing/breadcrumbRendererFactories.js` to compose and return all deterministic mapped renderer groups in one place:
+ - `simpleRouteRenderersByPath`
+ - `simpleLinkTextPairRenderersByPath`
+ - `simpleMyPortalRouteRenderersByPath`
+ - `caseDetailRouteRenderersByPath`
+ - `detailAndAccountRouteRenderersByPath`
+- Updated `components/breadcrumbs.js` to consume `createMappedRouteRendererGroups(...)` and remove local per-group factory composition boilerplate while preserving existing map precedence and resolver flow.
+- Expanded `tests/phase22/breadcrumbs-route-map-structure.test.cjs` to assert:
+ - grouped renderer maps are composed via `createMappedRouteRendererGroups(...)`
+ - mapped rendering path explicitly preserves null fallback (`routeRenderer ? routeRenderer() : null`)
+
+Validation:
+
+- `npx eslint lib/routing/breadcrumbRendererFactories.js components/breadcrumbs.js tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/breadcrumb-route-maps-helper.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite; breadcrumbs-route-map 4/4).
+
+Follow-ups:
+
+- Next slice option A (larger): extract remaining deterministic inline branches (`/myportal/case`, `/case/id/[incident]` if kept deterministic after constraints review) only if they remain callback/state free.
+- Next slice option B (smaller): add one focused assertion in helper-level tests that empty grouped map entries are tolerated without affecting precedence resolution.
+
+### CL-22541-S: breadcrumbs deterministic case-branch extraction (`/myportal/case`, `/case/id/[incident]`)
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/breadcrumbRendererFactories.js`, `components/breadcrumbs.js`, `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+type: change
+rationale: Execute the next deterministic extraction slice by migrating remaining callback-free case breadcrumb branches into the case-detail factory group while preserving map precedence and leaving stateful/callback routes inline.
+impact: Refactor-only structural extraction and test guardrail expansion; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Extended `createCaseDetailRouteRenderers(...)` in `lib/routing/breadcrumbRendererFactories.js` to map:
+ - `/myportal/case`
+ - `/case/id/[incident]`
+- Injected additional deterministic inputs required by these routes:
+ - `currentReference`
+ - `nestedSearchString`
+- Updated `createMappedRouteRendererGroups(...)` wiring to pass the new inputs to case-detail factory composition.
+- Updated `components/breadcrumbs.js` to pass `nestedSearchString` into grouped factory composition.
+- Removed now-redundant inline `isPath(...)` branches for:
+ - `/myportal/case`
+ - `/case/id/[incident]`
+- Expanded `tests/phase22/breadcrumbs-route-map-structure.test.cjs` to assert:
+ - both new routes are mapped in `createCaseDetailRouteRenderers`
+ - both inline branches are absent from `components/breadcrumbs.js`
+
+Validation:
+
+- `npx eslint lib/routing/breadcrumbRendererFactories.js components/breadcrumbs.js tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite; breadcrumbs-route-map 4/4).
+
+Follow-ups:
+
+- Next slice option A (larger): evaluate whether any remaining deterministic inline branches can be extracted without violating callback/state constraints.
+- Next slice option B (smaller): add a focused helper-level precedence test where earlier map contains falsy/non-function values and later maps contain valid renderers.
+
+### CL-22541-T: breadcrumbs larger slice — new-appeal mapped group extraction (`/newappeal/selectappeal`)
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/{breadcrumbRendererFactories,breadcrumbRouteMaps}.js`, `components/breadcrumbs.js`, `tests/phase22/{breadcrumb-route-maps-helper,breadcrumbs-route-map-structure}.test.cjs`
+type: change
+rationale: Execute the next larger deterministic refactor slice by extracting callback-free new-appeal breadcrumb branching into a dedicated mapped renderer group and extending map-order guardrails.
+impact: Refactor-only structural extraction plus precedence-test hardening; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `createNewAppealRouteRenderers(...)` in `lib/routing/breadcrumbRendererFactories.js`.
+- Mapped `/newappeal/selectappeal` into the new grouped renderer factory.
+- Extended grouped composition return from `createMappedRouteRendererGroups(...)` with `newAppealRouteRenderersByPath`.
+- Updated `lib/routing/breadcrumbRouteMaps.js` to include `newAppealRouteRenderersByPath` in explicit map precedence ordering.
+- Updated `components/breadcrumbs.js` to consume/pass `newAppealRouteRenderersByPath` into map builder.
+- Removed now-redundant inline `isPath("/newappeal/selectappeal")` branch from component JSX.
+- Expanded tests:
+ - `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+ - assert new factory export and mapped route presence
+ - assert inline branch removal
+ - assert updated map-order invariant (simple -> link-text -> myportal -> new-appeal -> case detail -> detail/account)
+ - `tests/phase22/breadcrumb-route-maps-helper.test.cjs`
+ - assert six-map ordering contract and updated grouped precedence wiring.
+
+Validation:
+
+- `npx eslint lib/routing/breadcrumbRendererFactories.js lib/routing/breadcrumbRouteMaps.js components/breadcrumbs.js tests/phase22/breadcrumb-route-maps-helper.test.cjs tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next larger slice candidate: evaluate extraction viability for `/case` branch only if callback/`router.back()` behavior can remain explicit and parity-safe.
+- Next smaller slice candidate: add helper-level test coverage for unexpected non-function map values and explicit skip behavior (defensive precedence guard).
+
+### CL-22541-U: breadcrumbs smaller slice — defensive mapped-route resolver hardening
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/breadcrumbRouteMaps.js`, `tests/phase22/breadcrumb-route-maps-helper.test.cjs`
+type: change
+rationale: Execute the queued smaller slice by hardening mapped-route lookup against malformed map entries and non-function values while keeping precedence behavior unchanged.
+impact: Refactor-hardening only; no intended breadcrumb route behavior change for valid mapped renderers.
+status: completed
+
+Summary:
+
+- Updated `getMappedRendererByPath(...)` to safely handle invalid route-map entries:
+ - returns `null` when map is missing/non-object
+ - returns `null` when mapped value is not a function
+- Preserved existing resolver precedence semantics for valid function renderers.
+- Added focused helper test asserting resolver skips:
+ - invalid map entries (`null`/`undefined`)
+ - non-function mapped values
+ and still resolves the first later valid renderer.
+
+Validation:
+
+- `npx eslint lib/routing/breadcrumbRouteMaps.js tests/phase22/breadcrumb-route-maps-helper.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite; breadcrumb-route-maps-helper 5/5).
+
+Follow-ups:
+
+- Next larger slice: extract remaining callback-bearing deterministic branches into mapped route groups with callback injection, then update structure guards accordingly.
+
+### CL-22541-V: breadcrumbs larger slice — callback route-group extraction (`/myportal/case/id/[incident]`, `/case`)
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/{breadcrumbRendererFactories,breadcrumbRouteMaps}.js`, `components/breadcrumbs.js`, `tests/phase22/{breadcrumb-route-maps-helper,breadcrumbs-route-map-structure}.test.cjs`
+type: change
+rationale: Execute the next larger extraction slice by moving callback-bearing breadcrumb branches into a dedicated callback renderer map while preserving `router.back()` behavior through explicit callback injection.
+impact: Structural refactor with preserved callback semantics; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `createCallbackRouteRenderers(...)` in `lib/routing/breadcrumbRendererFactories.js` to map:
+ - `/myportal/case/id/[incident]`
+ - `/case`
+- Introduced explicit callback injection (`onBack`) into grouped factory composition and route renderers, preserving `router.back()` behavior via injected callback.
+- Extended `createMappedRouteRendererGroups(...)` return with `callbackRouteRenderersByPath`.
+- Updated map builder in `lib/routing/breadcrumbRouteMaps.js` to include callback map in explicit precedence order:
+ - simple -> link-text -> myportal -> new-appeal -> callback -> case detail -> detail/account.
+- Updated `components/breadcrumbs.js`:
+ - pass `onBack: () => { router.back(); }` into grouped factory composition
+ - include callback route map in `buildBreadcrumbRendererMaps(...)`
+ - remove now-redundant inline `isPath("/myportal/case/id/[incident]")` and `isPath("/case")` branches.
+- Expanded tests:
+ - `tests/phase22/breadcrumb-route-maps-helper.test.cjs`
+ - update grouped map order assertions to seven maps including callback group.
+ - `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+ - assert callback factory export and mapped callback route presence
+ - assert callback map inclusion in component grouped destructuring and map-order invariant
+ - assert inline callback branches are removed while `router.back();` remains preserved.
+
+Validation:
+
+- `npx eslint lib/routing/breadcrumbRendererFactories.js lib/routing/breadcrumbRouteMaps.js components/breadcrumbs.js tests/phase22/breadcrumb-route-maps-helper.test.cjs tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next smaller slice candidate: add a focused helper test for empty-string/whitespace path lookups to assert strict null behavior for non-exact keys.
+- Next larger slice candidate: evaluate whether `/newappeal/[appealtypes]` and `/myportal/[appealtypes]` back-link branches can be extracted with explicit callback/setter injection while preserving state-step semantics.
+
+### CL-22541-W: breadcrumbs larger slice — step-back route-group extraction (`/newappeal/[appealtypes]`, `/myportal/[appealtypes]`)
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/{breadcrumbRendererFactories,breadcrumbRouteMaps}.js`, `components/breadcrumbs.js`, `tests/phase22/{breadcrumb-route-maps-helper,breadcrumbs-route-map-structure}.test.cjs`
+type: change
+rationale: Execute the next larger extraction slice by moving step-based back-link branches into a dedicated mapped route group with explicit setter callback injection, preserving section-navigation semantics.
+impact: Structural refactor with preserved back-link and step-state behavior; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `createStepBackRouteRenderers(...)` in `lib/routing/breadcrumbRendererFactories.js` to map:
+ - `/newappeal/[appealtypes]`
+ - `/myportal/[appealtypes]`
+- Preserved existing step semantics in mapped handlers:
+ - no crumb when `currentSection <= 1`
+ - service-name crumb when `currentSection === 9999`
+ - back-link crumb with existing class parity (`govuk-breadcrumbs__link-item` for new-appeal flow)
+- Introduced explicit injected handlers (`onStepBack`, `onStepBackWithInlineClass`) and injected crumb renderer dependency (`renderBackCrumb`) into grouped factory composition.
+- Extended map builder in `lib/routing/breadcrumbRouteMaps.js` with `stepBackRouteRenderersByPath` and updated precedence order:
+ - simple -> link-text -> myportal -> new-appeal -> callback -> step-back -> case detail -> detail/account.
+- Updated `components/breadcrumbs.js`:
+ - pass `currentSection` and step-back callbacks into `createMappedRouteRendererGroups(...)`
+ - include step-back map in `buildBreadcrumbRendererMaps(...)`
+ - remove inline `isPath("/newappeal/[appealtypes]")` and `isPath("/myportal/[appealtypes]")` branches.
+- Expanded tests:
+ - `tests/phase22/breadcrumb-route-maps-helper.test.cjs`
+ - updated grouped map count/order to include step-back map (8 total)
+ - `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+ - assert step-back factory export + mapped route presence
+ - assert grouped destructuring includes step-back map
+ - assert inline step-back branches are removed
+ - assert updated map-order invariant includes step-back group.
+
+Validation:
+
+- `npx eslint lib/routing/breadcrumbRendererFactories.js lib/routing/breadcrumbRouteMaps.js components/breadcrumbs.js tests/phase22/breadcrumb-route-maps-helper.test.cjs tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next smaller slice candidate: add focused helper test for strict null behavior on empty-string/whitespace/non-exact path keys.
+- Next larger slice candidate: evaluate extractability of `/myportal/representation` back-link states via explicit callback injection, only if questionnaire/submit side-effects remain parity-safe and readable.
+
+### CL-22541-X: breadcrumbs smaller slice — strict null lookup guard coverage (empty/whitespace/non-exact paths)
+
+date: 2026-04-09
+author: Cline
+scope: `tests/phase22/breadcrumb-route-maps-helper.test.cjs`
+type: change
+rationale: Execute the queued smaller follow-up slice by hardening helper regression coverage for strict exact-match route lookup semantics.
+impact: Test-only hardening; no runtime route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added focused helper-level test case in `tests/phase22/breadcrumb-route-maps-helper.test.cjs`:
+ - `resolveMappedRouteRenderer returns null for empty, whitespace, and non-exact path variants`
+- New assertions verify resolver returns `null` for:
+ - empty path (`""`)
+ - whitespace-only path (`" "`)
+ - trailing/leading whitespace variants (`"/known "`, `" /known"`)
+ - case-variant non-exact key (`"/KNOWN"`)
+- Confirms route-map resolver remains strict/exact and does not auto-trim or case-normalize lookup keys.
+
+Validation:
+
+- `npx eslint tests/phase22/breadcrumb-route-maps-helper.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite; breadcrumb-route-maps-helper 6/6).
+
+Follow-ups:
+
+- Next larger slice candidate remains `/myportal/representation` callback/state branch extraction only if side-effects can be injected/parity-preserved cleanly.
+
+### CL-22541-Y: breadcrumbs larger slice — representation route-group extraction (`/myportal/representation`)
+
+date: 2026-04-09
+author: Cline
+scope: `lib/routing/{breadcrumbRendererFactories,breadcrumbRouteMaps}.js`, `components/breadcrumbs.js`, `tests/phase22/{breadcrumb-route-maps-helper,breadcrumbs-route-map-structure}.test.cjs`
+type: change
+rationale: Execute the queued larger follow-up slice by extracting `/myportal/representation` callback/state breadcrumb branches into a dedicated mapped route group with explicit callback injection while preserving questionnaire/submit side-effects.
+impact: Structural refactor with preserved representation back-link state behavior; no intended route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `createRepresentationRouteRenderers(...)` in `lib/routing/breadcrumbRendererFactories.js` mapping:
+ - `/myportal/representation`
+- Preserved existing representation-state semantics in mapped handler:
+ - submit/back from confirmation (`setRepresentationSubmit(qcount)`)
+ - non-questionnaire back path (`setRepresentationSubmit(false)`)
+ - questionnaire step-back path (`setShowQuestionnaireSection(showQuestionnaireSection - 1)` + `setRepresentationSubmit(false)`)
+- Introduced explicit injected state/callback dependencies into grouped composition:
+ - `currentView`, `hasRepDetails`, `repDetails`, `showQuestionnaireSection`
+ - `onRepresentationSubmitBackFromConfirmation`
+ - `onRepresentationSubmitBackToDetails`
+ - `onRepresentationQuestionnaireBack`
+- Extended `buildBreadcrumbRendererMaps(...)` in `lib/routing/breadcrumbRouteMaps.js` with `representationRouteRenderersByPath` and updated precedence order:
+ - simple -> link-text -> myportal -> new-appeal -> callback -> step-back -> representation -> case detail -> detail/account.
+- Updated `components/breadcrumbs.js`:
+ - pass representation state/callback injections into `createMappedRouteRendererGroups(...)`
+ - include `representationRouteRenderersByPath` in map builder call
+ - remove inline `isPath("/myportal/representation")` branch block.
+- Expanded tests:
+ - `tests/phase22/breadcrumb-route-maps-helper.test.cjs`
+ - updated map-count/order assertions to include representation group (9 total)
+ - updated map build fixtures to include `representationRouteRenderersByPath`
+ - `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+ - assert representation factory export + mapped route presence
+ - assert grouped destructuring includes representation map
+ - assert inline representation branch removal
+ - assert updated map-order invariant includes representation group.
+
+Validation:
+
+- `npx eslint lib/routing/breadcrumbRendererFactories.js lib/routing/breadcrumbRouteMaps.js components/breadcrumbs.js tests/phase22/breadcrumb-route-maps-helper.test.cjs tests/phase22/breadcrumbs-route-map-structure.test.cjs tests/phase22/index.test.cjs` -> pass.
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Next smaller slice candidate: helper-level assertion for missing/undefined representation dependency values yielding no renderer output regressions.
+- Next larger slice candidate: evaluate whether remaining `/newappeal` edge branch can be absorbed into mapped grouping without reducing readability or explicitness.
+
+### CL-22541-Z: breadcrumbs closure slice 1 — mapped-route precedence/callback invariants decision
+
+date: 2026-04-09
+author: Cline
+scope: `memory-bank/decisions.md`
+type: change
+rationale: Execute closure slice 1 by documenting explicit architectural invariants for breadcrumb mapped-route precedence and callback injection contracts so future refactors remain safe and auditable.
+impact: Documentation/governance hardening only; no runtime route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added `D-006` to `memory-bank/decisions.md`.
+- Captured accepted invariant contracts for:
+ - explicit breadcrumb map precedence ordering (9 groups)
+ - callback/state behavior dependency injection requirement (no ad-hoc inline reintroduction)
+ - structure test guardrails as contract checks for intentional future changes.
+
+Validation:
+
+- Documentation consistency review against current breadcrumb map structure and phase22 structure test expectations.
+
+Follow-ups:
+
+- Closure slice 2: add focused negative-path representation map/dependency tolerance test coverage.
+- Closure slice 3: resolve and guard remaining `/newappeal` inline edge branch treatment.
+
+### CL-22541-Z2: breadcrumbs closure slice 2 — missing representation map negative-path guard
+
+date: 2026-04-09
+author: Cline
+scope: `tests/phase22/breadcrumb-route-maps-helper.test.cjs`
+type: change
+rationale: Execute closure slice 2 by adding focused negative-path coverage ensuring resolver behavior remains stable when representation route-map dependency is missing from grouped map input.
+impact: Test hardening only; no runtime route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Added focused helper-level test:
+ - `resolveMappedRouteRenderer tolerates missing representation map entry in grouped map input`
+- New assertions verify:
+ - missing `representationRouteRenderersByPath` resolves `/myportal/representation` to `null`
+ - later maps still resolve correctly (case detail fallback remains intact).
+
+Validation:
+
+- `node tests/phase22/breadcrumb-route-maps-helper.test.cjs` -> pass (7/7).
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Closure slice 3: resolve and guard remaining `/newappeal` inline edge branch treatment.
+
+### CL-22541-Z3: breadcrumbs closure slice 3 — remove lingering `/newappeal` no-op inline branch
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`, `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+type: change
+rationale: Execute closure slice 3 by resolving the remaining inline `/newappeal` edge branch (a no-op render guard) and locking its removal with explicit structure coverage.
+impact: Refactor/test hardening only; no runtime route/auth/session/API/EN-CY/a11y behavior change.
+status: completed
+
+Summary:
+
+- Removed lingering no-op branch from breadcrumbs JSX:
+ - deleted `{isPath("/newappeal") && <>>}`
+- Added structure guard assertion that `/newappeal` inline branch is absent.
+
+Validation:
+
+- `node tests/phase22/breadcrumbs-route-map-structure.test.cjs` -> pass (4/4).
+- `node tests/phase22/index.test.cjs` -> pass (combined suite).
+
+Follow-ups:
+
+- Breadcrumb route-state extraction closure slices (1/2/3) complete.
+
### CL-00X: 22500 `components/elements/index.js` Phase 1 helper extraction
date: 2026-04-07
diff --git a/memory-bank/decisions.md b/memory-bank/decisions.md
index 34f90d18..5f3c4068 100644
--- a/memory-bank/decisions.md
+++ b/memory-bank/decisions.md
@@ -170,3 +170,40 @@ Related:
- `context/test-coverage-map.md`
- `context/current-state-scorecard.md`
- `memory-bank/open-questions.md` (Q-002)
+
+---
+
+### D-006: Breadcrumb mapped-route precedence and callback injection invariants
+
+date: 2026-04-09
+author: Cline
+scope: `components/breadcrumbs.js`, `lib/routing/{breadcrumbRendererFactories,breadcrumbRouteMaps}.js`, `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
+type: decision
+rationale: Priority 3 route-state extraction moved most breadcrumb behavior into grouped mapped renderers; explicit invariants are required so future refactors do not silently reorder precedence or regress callback/state behavior.
+impact: Improves regression safety for search/case/myportal navigation by making map-order and callback injection constraints explicit.
+status: accepted
+
+Decision:
+
+- Preserve explicit mapped renderer precedence in this order unless a tested migration plan is approved:
+ 1. `simpleRouteRenderersByPath`
+ 2. `simpleLinkTextPairRenderersByPath`
+ 3. `simpleMyPortalRouteRenderersByPath`
+ 4. `newAppealRouteRenderersByPath`
+ 5. `callbackRouteRenderersByPath`
+ 6. `stepBackRouteRenderersByPath`
+ 7. `representationRouteRenderersByPath`
+ 8. `caseDetailRouteRenderersByPath`
+ 9. `detailAndAccountRouteRenderersByPath`
+- Callback/stateful breadcrumb behavior must be dependency-injected into factory groups (e.g. `onBack`, step-back callbacks, representation submit/questionnaire callbacks) rather than recreated as ad-hoc inline path branches.
+- Structural guardrails in `tests/phase22/breadcrumbs-route-map-structure.test.cjs` are considered contract tests for these invariants and must be updated in the same commit as intentional precedence/callback changes.
+
+Consequences:
+
+- Future breadcrumb extraction work remains predictable and test-auditable.
+- Accidental map-order drift or callback behavior inlining should be caught early by structure/helper tests.
+
+Related:
+
+- `memory-bank/change-log.md` (CL-22541-V, CL-22541-W, CL-22541-Y)
+- `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
diff --git a/tests/phase22/auth-redirect-safety.test.cjs b/tests/phase22/auth-redirect-safety.test.cjs
index 7020109c..9cbf80d8 100644
--- a/tests/phase22/auth-redirect-safety.test.cjs
+++ b/tests/phase22/auth-redirect-safety.test.cjs
@@ -21,7 +21,7 @@ const loadAuthInternals = () => {
source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, "");
source = source.replace(
/export default NextAuthPEDW;\s*$/,
- "module.exports = { appendParamsAndPathToNewUrl, resolveLocale, authOptions, NextAuthPEDW };"
+ "module.exports = { appendParamsAndPathToNewUrl, resolveRequestLocale, authOptions, NextAuthPEDW };"
);
const context = {
@@ -68,11 +68,11 @@ const loadAuthInternals = () => {
return context.module.exports;
};
-test("auth/resolveLocale prefers query then body then cookie then default", async () => {
+test("auth/resolveRequestLocale prefers query then body then cookie then default", async () => {
const mod = loadAuthInternals();
assert.strictEqual(
- mod.resolveLocale({
+ mod.resolveRequestLocale({
query: { locale: "cy" },
body: { locale: "en" },
cookies: { pedw_locale: "en" }
@@ -81,7 +81,7 @@ test("auth/resolveLocale prefers query then body then cookie then default", asyn
);
assert.strictEqual(
- mod.resolveLocale({
+ mod.resolveRequestLocale({
body: { locale: "cy" },
cookies: { pedw_locale: "en" }
}),
@@ -89,13 +89,13 @@ test("auth/resolveLocale prefers query then body then cookie then default", asyn
);
assert.strictEqual(
- mod.resolveLocale({
+ mod.resolveRequestLocale({
cookies: { pedw_locale: "cy" }
}),
"cy"
);
- assert.strictEqual(mod.resolveLocale({}), "en");
+ assert.strictEqual(mod.resolveRequestLocale({}), "en");
});
test("auth/redirect callback keeps relative URLs on same base", async () => {
diff --git a/tests/phase22/breadcrumb-route-maps-helper.test.cjs b/tests/phase22/breadcrumb-route-maps-helper.test.cjs
new file mode 100644
index 00000000..7c6439bc
--- /dev/null
+++ b/tests/phase22/breadcrumb-route-maps-helper.test.cjs
@@ -0,0 +1,365 @@
+const assert = require("assert");
+const fs = require("fs");
+const path = require("path");
+const vm = require("vm");
+
+const tests = [];
+const test = (name, fn) => tests.push({ name, fn });
+
+const helperModulePath = path.join(
+ __dirname,
+ "..",
+ "..",
+ "lib",
+ "routing",
+ "breadcrumbRouteMaps.js"
+);
+
+const loadHelpers = () => {
+ const source = fs.readFileSync(helperModulePath, "utf8");
+
+ const transformedSource = `
+${source
+ .replace(/export const /g, "const ")
+ .replace(/export function /g, "function ")}
+
+module.exports = {
+ buildBreadcrumbRendererMaps,
+ resolveMappedRouteRenderer
+};
+`;
+
+ const sandbox = {
+ module: { exports: {} },
+ exports: {},
+ require,
+ __dirname: path.dirname(helperModulePath),
+ __filename: helperModulePath,
+ console
+ };
+
+ vm.runInNewContext(transformedSource, sandbox, {
+ filename: helperModulePath
+ });
+
+ return sandbox.module.exports;
+};
+
+test("buildBreadcrumbRendererMaps returns grouped maps in explicit precedence order", () => {
+ const { buildBreadcrumbRendererMaps } = loadHelpers();
+
+ const simpleRouteRenderersByPath = {
+ "/alpha": () => "alpha"
+ };
+ const simpleLinkTextPairRenderersByPath = {
+ "/beta": () => "beta"
+ };
+ const simpleMyPortalRouteRenderersByPath = {
+ "/gamma": () => "gamma"
+ };
+ const newAppealRouteRenderersByPath = {
+ "/eta": () => "eta"
+ };
+ const callbackRouteRenderersByPath = {
+ "/theta": () => "theta"
+ };
+ const stepBackRouteRenderersByPath = {
+ "/iota": () => "iota"
+ };
+ const representationRouteRenderersByPath = {
+ "/kappa": () => "kappa"
+ };
+ const caseDetailRouteRenderersByPath = {
+ "/delta": () => "delta"
+ };
+ const detailAndAccountRouteRenderersByPath = {
+ "/epsilon": () => "epsilon"
+ };
+
+ const result = buildBreadcrumbRendererMaps({
+ simpleRouteRenderersByPath,
+ simpleLinkTextPairRenderersByPath,
+ simpleMyPortalRouteRenderersByPath,
+ newAppealRouteRenderersByPath,
+ callbackRouteRenderersByPath,
+ stepBackRouteRenderersByPath,
+ representationRouteRenderersByPath,
+ caseDetailRouteRenderersByPath,
+ detailAndAccountRouteRenderersByPath
+ });
+
+ assert.ok(Array.isArray(result), "Expected grouped maps array");
+ assert.strictEqual(result.length, 9, "Expected nine grouped route maps");
+ assert.strictEqual(
+ result[0],
+ simpleRouteRenderersByPath,
+ "Expected simple route renderers first"
+ );
+ assert.strictEqual(
+ result[1],
+ simpleLinkTextPairRenderersByPath,
+ "Expected simple link-text pair renderers second"
+ );
+ assert.strictEqual(
+ result[2],
+ simpleMyPortalRouteRenderersByPath,
+ "Expected my-portal route renderers third"
+ );
+ assert.strictEqual(
+ result[3],
+ newAppealRouteRenderersByPath,
+ "Expected new-appeal route renderers fourth"
+ );
+ assert.strictEqual(
+ result[4],
+ callbackRouteRenderersByPath,
+ "Expected callback route renderers fifth"
+ );
+ assert.strictEqual(
+ result[5],
+ stepBackRouteRenderersByPath,
+ "Expected step-back route renderers sixth"
+ );
+ assert.strictEqual(
+ result[6],
+ representationRouteRenderersByPath,
+ "Expected representation route renderers seventh"
+ );
+ assert.strictEqual(
+ result[7],
+ caseDetailRouteRenderersByPath,
+ "Expected case detail route renderers eighth"
+ );
+ assert.strictEqual(
+ result[8],
+ detailAndAccountRouteRenderersByPath,
+ "Expected detail and account route renderers ninth"
+ );
+});
+
+test("resolveMappedRouteRenderer returns renderer from first matching grouped map", () => {
+ const { buildBreadcrumbRendererMaps, resolveMappedRouteRenderer } =
+ loadHelpers();
+
+ const firstRenderer = () => "first";
+ const secondRenderer = () => "second";
+
+ const maps = buildBreadcrumbRendererMaps({
+ simpleRouteRenderersByPath: {
+ "/known": firstRenderer
+ },
+ simpleLinkTextPairRenderersByPath: {
+ "/known": secondRenderer
+ },
+ simpleMyPortalRouteRenderersByPath: {},
+ newAppealRouteRenderersByPath: {},
+ callbackRouteRenderersByPath: {},
+ stepBackRouteRenderersByPath: {},
+ representationRouteRenderersByPath: {},
+ caseDetailRouteRenderersByPath: {},
+ detailAndAccountRouteRenderersByPath: {}
+ });
+
+ const result = resolveMappedRouteRenderer("/known", maps);
+
+ assert.strictEqual(
+ result,
+ firstRenderer,
+ "Expected resolver to use first matching map by precedence order"
+ );
+});
+
+test("resolveMappedRouteRenderer returns renderer from later grouped map when earlier maps do not match", () => {
+ const { buildBreadcrumbRendererMaps, resolveMappedRouteRenderer } =
+ loadHelpers();
+
+ const detailRenderer = () => "detail";
+
+ const maps = buildBreadcrumbRendererMaps({
+ simpleRouteRenderersByPath: {},
+ simpleLinkTextPairRenderersByPath: {},
+ newAppealRouteRenderersByPath: {
+ "/newappeal/selectappeal": () => "newappeal"
+ },
+ simpleMyPortalRouteRenderersByPath: {},
+ caseDetailRouteRenderersByPath: {},
+ callbackRouteRenderersByPath: {},
+ stepBackRouteRenderersByPath: {},
+ representationRouteRenderersByPath: {},
+ detailAndAccountRouteRenderersByPath: {
+ "/account/personaldetails": detailRenderer
+ }
+ });
+
+ const result = resolveMappedRouteRenderer("/account/personaldetails", maps);
+
+ assert.strictEqual(
+ result,
+ detailRenderer,
+ "Expected resolver to return matching renderer from later grouped map"
+ );
+});
+
+test("resolveMappedRouteRenderer returns null for unmapped path", () => {
+ const { buildBreadcrumbRendererMaps, resolveMappedRouteRenderer } =
+ loadHelpers();
+
+ const maps = buildBreadcrumbRendererMaps({
+ simpleRouteRenderersByPath: {
+ "/known": () => "known"
+ },
+ simpleLinkTextPairRenderersByPath: {},
+ simpleMyPortalRouteRenderersByPath: {},
+ newAppealRouteRenderersByPath: {},
+ callbackRouteRenderersByPath: {},
+ stepBackRouteRenderersByPath: {},
+ representationRouteRenderersByPath: {},
+ caseDetailRouteRenderersByPath: {},
+ detailAndAccountRouteRenderersByPath: {}
+ });
+
+ const result = resolveMappedRouteRenderer("/unknown", maps);
+
+ assert.strictEqual(
+ result,
+ null,
+ "Expected null when no grouped map contains the path"
+ );
+});
+
+test("resolveMappedRouteRenderer returns null for empty, whitespace, and non-exact path variants", () => {
+ const { buildBreadcrumbRendererMaps, resolveMappedRouteRenderer } =
+ loadHelpers();
+
+ const knownRenderer = () => "known";
+
+ const maps = buildBreadcrumbRendererMaps({
+ simpleRouteRenderersByPath: {
+ "/known": knownRenderer
+ },
+ simpleLinkTextPairRenderersByPath: {},
+ simpleMyPortalRouteRenderersByPath: {},
+ newAppealRouteRenderersByPath: {},
+ callbackRouteRenderersByPath: {},
+ stepBackRouteRenderersByPath: {},
+ representationRouteRenderersByPath: {},
+ caseDetailRouteRenderersByPath: {},
+ detailAndAccountRouteRenderersByPath: {}
+ });
+
+ assert.strictEqual(
+ resolveMappedRouteRenderer("", maps),
+ null,
+ "Expected null for empty path lookup when no empty key is mapped"
+ );
+
+ assert.strictEqual(
+ resolveMappedRouteRenderer(" ", maps),
+ null,
+ "Expected null for whitespace-only path lookup"
+ );
+
+ assert.strictEqual(
+ resolveMappedRouteRenderer("/known ", maps),
+ null,
+ "Expected null for non-exact path variant with trailing whitespace"
+ );
+
+ assert.strictEqual(
+ resolveMappedRouteRenderer(" /known", maps),
+ null,
+ "Expected null for non-exact path variant with leading whitespace"
+ );
+
+ assert.strictEqual(
+ resolveMappedRouteRenderer("/KNOWN", maps),
+ null,
+ "Expected null for non-exact case-variant path lookup"
+ );
+});
+
+test("resolveMappedRouteRenderer skips invalid map entries and non-function renderer values", () => {
+ const { buildBreadcrumbRendererMaps, resolveMappedRouteRenderer } =
+ loadHelpers();
+
+ const validRenderer = () => "valid";
+
+ const maps = buildBreadcrumbRendererMaps({
+ simpleRouteRenderersByPath: {
+ "/known": "not-a-function"
+ },
+ simpleLinkTextPairRenderersByPath: null,
+ simpleMyPortalRouteRenderersByPath: {
+ "/known": validRenderer
+ },
+ newAppealRouteRenderersByPath: undefined,
+ callbackRouteRenderersByPath: {},
+ stepBackRouteRenderersByPath: {},
+ representationRouteRenderersByPath: {},
+ caseDetailRouteRenderersByPath: {},
+ detailAndAccountRouteRenderersByPath: {}
+ });
+
+ const result = resolveMappedRouteRenderer("/known", maps);
+
+ assert.strictEqual(
+ result,
+ validRenderer,
+ "Expected resolver to skip invalid/non-function entries and return first valid renderer"
+ );
+});
+
+test("resolveMappedRouteRenderer tolerates missing representation map entry in grouped map input", () => {
+ const { buildBreadcrumbRendererMaps, resolveMappedRouteRenderer } =
+ loadHelpers();
+
+ const caseDetailRenderer = () => "case-detail";
+
+ const maps = buildBreadcrumbRendererMaps({
+ simpleRouteRenderersByPath: {},
+ simpleLinkTextPairRenderersByPath: {},
+ simpleMyPortalRouteRenderersByPath: {},
+ newAppealRouteRenderersByPath: {},
+ callbackRouteRenderersByPath: {},
+ stepBackRouteRenderersByPath: {},
+ representationRouteRenderersByPath: undefined,
+ caseDetailRouteRenderersByPath: {
+ "/case/[ticketnumber]": caseDetailRenderer
+ },
+ detailAndAccountRouteRenderersByPath: {}
+ });
+
+ assert.strictEqual(
+ resolveMappedRouteRenderer("/myportal/representation", maps),
+ null,
+ "Expected null when representation renderer map is missing"
+ );
+
+ assert.strictEqual(
+ resolveMappedRouteRenderer("/case/[ticketnumber]", maps),
+ caseDetailRenderer,
+ "Expected later grouped map resolution to remain intact when representation map is missing"
+ );
+});
+
+const run = async () => {
+ let passed = 0;
+
+ for (const currentTest of tests) {
+ await currentTest.fn();
+ passed += 1;
+ }
+
+ console.log(
+ `Phase 22 breadcrumb-route-maps-helper tests passed (${passed}/${tests.length}).`
+ );
+};
+
+module.exports = run;
+
+if (require.main === module) {
+ run().catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
+}
diff --git a/tests/phase22/breadcrumbs-route-map-structure.test.cjs b/tests/phase22/breadcrumbs-route-map-structure.test.cjs
new file mode 100644
index 00000000..4dfd31a3
--- /dev/null
+++ b/tests/phase22/breadcrumbs-route-map-structure.test.cjs
@@ -0,0 +1,527 @@
+const assert = require("assert");
+const fs = require("fs");
+const path = require("path");
+
+const tests = [];
+const test = (name, fn) => tests.push({ name, fn });
+
+const loadSource = (...parts) => {
+ const filePath = path.join(__dirname, "..", "..", ...parts);
+ return fs.readFileSync(filePath, "utf8");
+};
+
+const loadBreadcrumbSource = () => loadSource("components", "breadcrumbs.js");
+
+const loadFactorySource = () =>
+ loadSource("lib", "routing", "breadcrumbRendererFactories.js");
+
+test("breadcrumbs/factory module includes expected mapped route definitions", async () => {
+ const source = loadFactorySource();
+
+ assert.strictEqual(
+ source.includes("export const createSimpleRouteRenderers ="),
+ true,
+ "Expected simple route renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes("export const createSimpleLinkTextPairRenderers ="),
+ true,
+ "Expected link-text pair renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes("export const createSimpleMyPortalRouteRenderers ="),
+ true,
+ "Expected my-portal route renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes("export const createNewAppealRouteRenderers ="),
+ true,
+ "Expected new-appeal route renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes("export const createCallbackRouteRenderers ="),
+ true,
+ "Expected callback route renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes("export const createStepBackRouteRenderers ="),
+ true,
+ "Expected step-back route renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes("export const createRepresentationRouteRenderers ="),
+ true,
+ "Expected representation route renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes("export const createCaseDetailRouteRenderers ="),
+ true,
+ "Expected case detail route renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes("export const createDetailAndAccountRouteRenderers ="),
+ true,
+ "Expected detail and account route renderer factory export"
+ );
+
+ assert.strictEqual(
+ source.includes('"/addresssearchresults": () => ('),
+ true,
+ "Expected /addresssearchresults to be mapped in createSimpleLinkTextPairRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/advancedsearchresults": () => ('),
+ true,
+ "Expected /advancedsearchresults to be mapped in createSimpleLinkTextPairRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/searchresults": () => ('),
+ true,
+ "Expected /myportal/searchresults to be mapped in createSimpleMyPortalRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/viewall": () =>'),
+ true,
+ "Expected /myportal/viewall to be mapped in createSimpleMyPortalRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/advancedsearchresults": () => ('),
+ true,
+ "Expected /myportal/advancedsearchresults to be mapped in createSimpleMyPortalRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/addresssearchresults": () => ('),
+ true,
+ "Expected /myportal/addresssearchresults to be mapped in createSimpleMyPortalRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/newappeal/selectappeal": () => ('),
+ true,
+ "Expected /newappeal/selectappeal to be mapped in createNewAppealRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/case/id/[incident]": () => ('),
+ true,
+ "Expected /myportal/case/id/[incident] to be mapped in createCallbackRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/case": () => ('),
+ true,
+ "Expected /case to be mapped in createCallbackRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/newappeal/[appealtypes]": () => {'),
+ true,
+ "Expected /newappeal/[appealtypes] to be mapped in createStepBackRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/[appealtypes]": () => {'),
+ true,
+ "Expected /myportal/[appealtypes] to be mapped in createStepBackRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/representation": () => ('),
+ true,
+ "Expected /myportal/representation to be mapped in createRepresentationRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/case/[ticketnumber]": () =>'),
+ true,
+ "Expected /case/[ticketnumber] to be mapped in createCaseDetailRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/case": () =>'),
+ true,
+ "Expected /myportal/case to be mapped in createCaseDetailRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/case/id/[incident]": () =>'),
+ true,
+ "Expected /case/id/[incident] to be mapped in createCaseDetailRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/case/[ticketnumber]": () =>'),
+ true,
+ "Expected /myportal/case/[ticketnumber] to be mapped in createCaseDetailRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/dnsdetails": () =>'),
+ true,
+ "Expected /myportal/dnsdetails to be mapped in createDetailAndAccountRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/myportal/dns/[developmentName]": () =>'),
+ true,
+ "Expected /myportal/dns/[developmentName] to be mapped in createDetailAndAccountRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/dnsdetails": () =>'),
+ true,
+ "Expected /dnsdetails to be mapped in createDetailAndAccountRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/dns/[developmentName]": () =>'),
+ true,
+ "Expected /dns/[developmentName] to be mapped in createDetailAndAccountRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/dns/application-view": () =>'),
+ true,
+ "Expected /dns/application-view to be mapped in createDetailAndAccountRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/account/personaldetails": () =>'),
+ true,
+ "Expected /account/personaldetails to be mapped in createDetailAndAccountRouteRenderers"
+ );
+
+ assert.strictEqual(
+ source.includes('"/account/changepassword": () =>'),
+ true,
+ "Expected /account/changepassword to be mapped in createDetailAndAccountRouteRenderers"
+ );
+});
+
+test("breadcrumbs/component composes mapped routes via imported factories and shared resolver", async () => {
+ const source = loadBreadcrumbSource();
+
+ assert.strictEqual(
+ source.includes('from "../lib/routing/breadcrumbRouteMaps"'),
+ true,
+ "Expected breadcrumbs to import shared breadcrumb route-map helper module"
+ );
+
+ assert.strictEqual(
+ source.includes('from "../lib/routing/breadcrumbRendererFactories"'),
+ true,
+ "Expected breadcrumbs to import breadcrumb renderer factories"
+ );
+
+ assert.strictEqual(
+ /createMappedRouteRendererGroups/.test(source),
+ true,
+ "Expected breadcrumbs to reference createMappedRouteRendererGroups"
+ );
+
+ assert.strictEqual(
+ /const\s+\{[\s\S]*simpleRouteRenderersByPath[\s\S]*newAppealRouteRenderersByPath[\s\S]*callbackRouteRenderersByPath[\s\S]*stepBackRouteRenderersByPath[\s\S]*representationRouteRenderersByPath[\s\S]*detailAndAccountRouteRenderersByPath[\s\S]*\}\s*=\s*createMappedRouteRendererGroups\s*\(/.test(
+ source
+ ),
+ true,
+ "Expected breadcrumbs to compose grouped renderer maps via createMappedRouteRendererGroups"
+ );
+
+ assert.strictEqual(
+ /const\s+mappedRouteRendererMaps\s*=\s*buildBreadcrumbRendererMaps\s*\(/.test(
+ source
+ ),
+ true,
+ "Expected grouped mapped route renderer map composition to use helper builder"
+ );
+
+ assert.strictEqual(
+ /return\s+resolveMappedRouteRendererByMaps\s*\(\s*path\s*,\s*mappedRouteRendererMaps\s*\)/.test(
+ source
+ ),
+ true,
+ "Expected resolver to delegate to shared route-map resolver helper"
+ );
+
+ assert.strictEqual(
+ /const\s+resolveMappedRouteRenderer\s*=\s*\(\s*path\s*\)\s*=>/.test(
+ source
+ ),
+ true,
+ "Expected unified mapped route resolver helper to exist"
+ );
+
+ assert.strictEqual(
+ /const\s+renderMappedRoute\s*=\s*\(\s*path\s*,\s*resolver\s*=\s*resolveMappedRouteRenderer\s*\)\s*=>/.test(
+ source
+ ),
+ true,
+ "Expected reusable renderMappedRoute helper to exist"
+ );
+
+ assert.strictEqual(
+ source.includes("return routeRenderer ? routeRenderer() : null;"),
+ true,
+ "Expected mapped renderer flow to return null for unmapped routes"
+ );
+
+ assert.strictEqual(
+ source.includes("{renderMappedRoute(pathname)}"),
+ true,
+ "Expected breadcrumb JSX to use single mapped route render invocation"
+ );
+
+ assert.strictEqual(
+ source.includes("const viewAllLabelResolver = labelsByKey[viewKey];"),
+ true,
+ "Expected view-all label lookup to resolve callable/non-callable map entries"
+ );
+
+ assert.strictEqual(
+ source.includes('return typeof viewAllLabelResolver === "function"'),
+ true,
+ "Expected view-all label resolver to invoke function-backed labels"
+ );
+
+ assert.strictEqual(
+ source.includes(
+ "const staticTextCrumbResolver = staticTextCrumbByPath[path];"
+ ),
+ true,
+ "Expected static text breadcrumb path lookup to resolve a callable label renderer"
+ );
+
+ assert.strictEqual(
+ source.includes(
+ "return staticTextCrumbResolver ? staticTextCrumbResolver() : null;"
+ ),
+ true,
+ "Expected static text breadcrumb resolver to invoke mapped label function"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/advancedsearchresults")'),
+ false,
+ "Expected /myportal/advancedsearchresults explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/addresssearchresults")'),
+ false,
+ "Expected /myportal/addresssearchresults explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/case/[ticketnumber]")'),
+ false,
+ "Expected /case/[ticketnumber] explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/case")'),
+ false,
+ "Expected /myportal/case explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/case/id/[incident]")'),
+ false,
+ "Expected /case/id/[incident] explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/newappeal/selectappeal")'),
+ false,
+ "Expected /newappeal/selectappeal explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/newappeal")'),
+ false,
+ "Expected /newappeal no-op explicit branch to be removed"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/newappeal/[appealtypes]")'),
+ false,
+ "Expected /newappeal/[appealtypes] explicit branch to be removed after step-back mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/[appealtypes]")'),
+ false,
+ "Expected /myportal/[appealtypes] explicit branch to be removed after step-back mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/representation")'),
+ false,
+ "Expected /myportal/representation explicit branch to be removed after representation mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/case/[ticketnumber]")'),
+ false,
+ "Expected /myportal/case/[ticketnumber] explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/dnsdetails")'),
+ false,
+ "Expected /myportal/dnsdetails explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/dns/[developmentName]")'),
+ false,
+ "Expected /myportal/dns/[developmentName] explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/dnsdetails")'),
+ false,
+ "Expected /dnsdetails explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/dns/[developmentName]")'),
+ false,
+ "Expected /dns/[developmentName] explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/dns/application-view")'),
+ false,
+ "Expected /dns/application-view explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/account/personaldetails")'),
+ false,
+ "Expected /account/personaldetails explicit branch to be removed after mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/account/changepassword")'),
+ false,
+ "Expected /account/changepassword explicit branch to be removed after mapping"
+ );
+});
+
+test("breadcrumbs/map builder preserves route map ordering", async () => {
+ const source = loadBreadcrumbSource();
+
+ const mapBuilderCallIndex = source.indexOf(
+ "const mappedRouteRendererMaps = buildBreadcrumbRendererMaps({"
+ );
+
+ assert.notStrictEqual(
+ mapBuilderCallIndex,
+ -1,
+ "Expected grouped map builder call to exist"
+ );
+
+ const mapBuilderSlice = source.slice(
+ mapBuilderCallIndex,
+ mapBuilderCallIndex + 700
+ );
+
+ const simpleRouteOrderIndex = mapBuilderSlice.indexOf(
+ "simpleRouteRenderersByPath"
+ );
+ const simpleLinkTextPairOrderIndex = mapBuilderSlice.indexOf(
+ "simpleLinkTextPairRenderersByPath"
+ );
+ const simpleMyPortalOrderIndex = mapBuilderSlice.indexOf(
+ "simpleMyPortalRouteRenderersByPath"
+ );
+ const newAppealOrderIndex = mapBuilderSlice.indexOf(
+ "newAppealRouteRenderersByPath"
+ );
+ const callbackOrderIndex = mapBuilderSlice.indexOf(
+ "callbackRouteRenderersByPath"
+ );
+ const stepBackOrderIndex = mapBuilderSlice.indexOf(
+ "stepBackRouteRenderersByPath"
+ );
+ const representationOrderIndex = mapBuilderSlice.indexOf(
+ "representationRouteRenderersByPath"
+ );
+ const caseDetailOrderIndex = mapBuilderSlice.indexOf(
+ "caseDetailRouteRenderersByPath"
+ );
+ const detailAndAccountOrderIndex = mapBuilderSlice.indexOf(
+ "detailAndAccountRouteRenderersByPath"
+ );
+
+ assert.strictEqual(
+ simpleRouteOrderIndex >= 0 &&
+ simpleLinkTextPairOrderIndex > simpleRouteOrderIndex &&
+ simpleMyPortalOrderIndex > simpleLinkTextPairOrderIndex &&
+ newAppealOrderIndex > simpleMyPortalOrderIndex &&
+ callbackOrderIndex > newAppealOrderIndex &&
+ stepBackOrderIndex > callbackOrderIndex &&
+ representationOrderIndex > stepBackOrderIndex &&
+ caseDetailOrderIndex > representationOrderIndex &&
+ detailAndAccountOrderIndex > caseDetailOrderIndex,
+ true,
+ "Expected map builder call to preserve explicit route-map ordering (simple -> link-text -> myportal -> new-appeal -> callback -> step-back -> representation -> case detail -> detail/account)"
+ );
+});
+
+test("breadcrumbs/callback-mapped branches preserve router.back behavior", async () => {
+ const source = loadBreadcrumbSource();
+
+ assert.strictEqual(
+ source.includes('isPath("/myportal/case/id/[incident]")'),
+ false,
+ "Expected /myportal/case/id/[incident] explicit branch to be removed after callback mapping"
+ );
+
+ assert.strictEqual(
+ source.includes('isPath("/case")'),
+ false,
+ "Expected /case explicit branch to be removed after callback mapping"
+ );
+
+ assert.strictEqual(
+ source.includes("router.back();"),
+ true,
+ "Expected callback-driven router.back() behavior to remain preserved"
+ );
+});
+
+const run = async () => {
+ let passed = 0;
+
+ for (const currentTest of tests) {
+ await currentTest.fn();
+ passed += 1;
+ }
+
+ console.log(
+ `Phase 22 breadcrumbs-route-map tests passed (${passed}/${tests.length}).`
+ );
+};
+
+module.exports = run;
+
+if (require.main === module) {
+ run().catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
+}
diff --git a/tests/phase22/index.test.cjs b/tests/phase22/index.test.cjs
index 02d4341c..7bfabdc5 100644
--- a/tests/phase22/index.test.cjs
+++ b/tests/phase22/index.test.cjs
@@ -6,6 +6,9 @@ const runPortalServiceTests = require("./portal-service-behaviour.test.cjs");
const runAuthRedirectSafetyTests = require("./auth-redirect-safety.test.cjs");
const runI18nRouteParityTests = require("./i18n-route-parity.test.cjs");
const runAzurestorageHelperTests = require("./azurestorage-helper-behaviour.test.cjs");
+const runRouteStateHelperTests = require("./route-state-helper.test.cjs");
+const runBreadcrumbRouteMapsHelperTests = require("./breadcrumb-route-maps-helper.test.cjs");
+const runBreadcrumbsRouteMapStructureTests = require("./breadcrumbs-route-map-structure.test.cjs");
const run = async () => {
await runCoreTokenTests();
@@ -16,6 +19,9 @@ const run = async () => {
await runAuthRedirectSafetyTests();
await runI18nRouteParityTests();
await runAzurestorageHelperTests();
+ await runRouteStateHelperTests();
+ await runBreadcrumbRouteMapsHelperTests();
+ await runBreadcrumbsRouteMapStructureTests();
console.log("Phase 22 combined suite passed.");
};
diff --git a/tests/phase22/route-state-helper.test.cjs b/tests/phase22/route-state-helper.test.cjs
new file mode 100644
index 00000000..83272777
--- /dev/null
+++ b/tests/phase22/route-state-helper.test.cjs
@@ -0,0 +1,392 @@
+const assert = require("assert");
+const fs = require("fs");
+const path = require("path");
+const vm = require("vm");
+
+const tests = [];
+const test = (name, fn) => tests.push({ name, fn });
+
+const helperModulePath = path.join(
+ __dirname,
+ "..",
+ "..",
+ "lib",
+ "routing",
+ "routeState.js"
+);
+
+const toPlain = (value) => JSON.parse(JSON.stringify(value));
+
+const loadHelpers = () => {
+ const source = fs.readFileSync(helperModulePath, "utf8");
+
+ const transformedSource = `
+${source
+ .replace(/export const /g, "const ")
+ .replace(/export function /g, "function ")}
+
+module.exports = {
+ normalizeRouteStateQuery,
+ resolveSearchResultsHref,
+ resolveSearchBreadcrumbLabel,
+ isDnsRoutePath,
+ resolveCaseBreadcrumbState
+};
+`;
+
+ const sandbox = {
+ module: { exports: {} },
+ exports: {},
+ require,
+ __dirname: path.dirname(helperModulePath),
+ __filename: helperModulePath,
+ console
+ };
+
+ vm.runInNewContext(transformedSource, sandbox, {
+ filename: helperModulePath
+ });
+
+ return sandbox.module.exports;
+};
+
+test("normalizeRouteStateQuery maps known boolean query flags", () => {
+ const { normalizeRouteStateQuery } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ normalizeRouteStateQuery({
+ va: "true",
+ adv: "false",
+ ads: "true",
+ key: "myCases"
+ })
+ ),
+ {
+ viewAll: true,
+ advanced: false,
+ address: true,
+ key: "myCases"
+ }
+ );
+});
+
+test("resolveSearchResultsHref falls back to myportal when no flags and no search query exist for signed-in user", () => {
+ const { resolveSearchResultsHref } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: {},
+ hasSession: true,
+ fallbackToMyPortalWhenNoFlags: true
+ })
+ ),
+ {
+ pathname: "/myportal"
+ }
+ );
+});
+
+test("resolveSearchResultsHref preserves public searchresults route when search query exists and no flags are set", () => {
+ const { resolveSearchResultsHref } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: { q: "abc" },
+ hasSession: false,
+ fallbackToMyPortalWhenNoFlags: true
+ })
+ ),
+ {
+ pathname: "/searchresults",
+ query: { q: "abc" }
+ }
+ );
+});
+
+test("resolveSearchResultsHref preserves myportal searchresults route when search query exists and no flags are set for signed-in user", () => {
+ const { resolveSearchResultsHref } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: { q: "abc" },
+ hasSession: true,
+ fallbackToMyPortalWhenNoFlags: true
+ })
+ ),
+ {
+ pathname: "/myportal/searchresults",
+ query: { q: "abc" }
+ }
+ );
+});
+
+test("resolveSearchResultsHref routes view-all requests to myportal viewall with key only", () => {
+ const { resolveSearchResultsHref } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: {
+ va: "true",
+ key: "mySubmittedReps",
+ q: "ignored"
+ },
+ hasSession: true,
+ includeViewAll: true
+ })
+ ),
+ {
+ pathname: "/myportal/viewall",
+ query: { key: "mySubmittedReps" }
+ }
+ );
+});
+
+test("resolveSearchResultsHref routes advanced requests to advancedsearchresults and preserves query", () => {
+ const { resolveSearchResultsHref } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: { q: "abc", adv: "true", page: "2" },
+ hasSession: false
+ })
+ ),
+ {
+ pathname: "/advancedsearchresults",
+ query: { q: "abc", adv: "true", page: "2" }
+ }
+ );
+});
+
+test("resolveSearchResultsHref routes address requests to addresssearchresults and preserves query", () => {
+ const { resolveSearchResultsHref } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: { postcode: "CF10", ads: "true" },
+ hasSession: true
+ })
+ ),
+ {
+ pathname: "/myportal/addresssearchresults",
+ query: { postcode: "CF10", ads: "true" }
+ }
+ );
+});
+
+test("resolveSearchResultsHref routes dns requests to dnsapplications", () => {
+ const { resolveSearchResultsHref } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: { anything: "x" },
+ hasSession: true,
+ isDnsRoute: true
+ })
+ ),
+ {
+ pathname: "/myportal/dnsapplications"
+ }
+ );
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: { anything: "x" },
+ hasSession: false,
+ isDnsRoute: true
+ })
+ ),
+ {
+ pathname: "/dnsapplications"
+ }
+ );
+});
+
+test("resolveSearchResultsHref defaults to searchresults and preserves query when no special route applies", () => {
+ const { resolveSearchResultsHref } = loadHelpers();
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveSearchResultsHref({
+ query: { q: "abc", page: "3" },
+ hasSession: false
+ })
+ ),
+ {
+ pathname: "/searchresults",
+ query: { q: "abc", page: "3" }
+ }
+ );
+});
+
+test("resolveSearchBreadcrumbLabel prioritizes view-all label, then keyed label, then advanced/address/default", () => {
+ const { resolveSearchBreadcrumbLabel } = loadHelpers();
+
+ const getViewAllLabel = (key) => {
+ const labels = {
+ myCases: "My cases",
+ watchedCases: "Watched cases"
+ };
+ return labels[key] || null;
+ };
+
+ assert.strictEqual(
+ resolveSearchBreadcrumbLabel({
+ query: { va: "true", key: "myCases" },
+ getViewAllLabel,
+ advancedLabel: "Advanced search results",
+ addressLabel: "Address search results",
+ defaultLabel: "Search results"
+ }),
+ "My cases"
+ );
+
+ assert.strictEqual(
+ resolveSearchBreadcrumbLabel({
+ query: { key: "watchedCases" },
+ getViewAllLabel,
+ advancedLabel: "Advanced search results",
+ addressLabel: "Address search results",
+ defaultLabel: "Search results"
+ }),
+ "Watched cases"
+ );
+
+ assert.strictEqual(
+ resolveSearchBreadcrumbLabel({
+ query: { adv: "true" },
+ getViewAllLabel,
+ advancedLabel: "Advanced search results",
+ addressLabel: "Address search results",
+ defaultLabel: "Search results"
+ }),
+ "Advanced search results"
+ );
+
+ assert.strictEqual(
+ resolveSearchBreadcrumbLabel({
+ query: { ads: "true" },
+ getViewAllLabel,
+ advancedLabel: "Advanced search results",
+ addressLabel: "Address search results",
+ defaultLabel: "Search results"
+ }),
+ "Address search results"
+ );
+
+ assert.strictEqual(
+ resolveSearchBreadcrumbLabel({
+ query: {},
+ getViewAllLabel,
+ advancedLabel: "Advanced search results",
+ addressLabel: "Address search results",
+ defaultLabel: "Search results"
+ }),
+ "Search results"
+ );
+});
+
+test("isDnsRoutePath correctly identifies dns route families", () => {
+ const { isDnsRoutePath } = loadHelpers();
+
+ assert.strictEqual(isDnsRoutePath("/dns"), true);
+ assert.strictEqual(isDnsRoutePath("/dns/applications"), true);
+ assert.strictEqual(isDnsRoutePath("/myportal/dns"), true);
+ assert.strictEqual(isDnsRoutePath("/myportal/dns/example"), true);
+
+ assert.strictEqual(isDnsRoutePath("/searchresults"), false);
+ assert.strictEqual(isDnsRoutePath("/myportal"), false);
+ assert.strictEqual(isDnsRoutePath("/myportal/case/[ticketnumber]"), false);
+});
+
+test("resolveCaseBreadcrumbState returns breadcrumb and case-results hrefs plus breadcrumb label", () => {
+ const { resolveCaseBreadcrumbState } = loadHelpers();
+
+ const getViewAllLabel = (key) =>
+ key === "mySubmittedReps" ? "Submitted representations" : null;
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveCaseBreadcrumbState({
+ query: { q: "cas-", adv: "false" },
+ hasSession: true,
+ getViewAllLabel,
+ advancedLabel: "Advanced search results",
+ addressLabel: "Address search results",
+ defaultLabel: "Search results"
+ })
+ ),
+ {
+ breadcrumbHref: {
+ pathname: "/myportal/searchresults",
+ query: { q: "cas-", adv: "false" }
+ },
+ caseResultsHref: {
+ pathname: "/myportal/searchresults",
+ query: { q: "cas-", adv: "false" }
+ },
+ breadcrumbLabel: "Search results"
+ }
+ );
+});
+
+test("resolveCaseBreadcrumbState distinguishes view-all breadcrumb from case-results href", () => {
+ const { resolveCaseBreadcrumbState } = loadHelpers();
+
+ const getViewAllLabel = (key) =>
+ key === "mySubmittedReps" ? "Submitted representations" : null;
+
+ assert.deepStrictEqual(
+ toPlain(
+ resolveCaseBreadcrumbState({
+ query: { va: "true", key: "mySubmittedReps" },
+ hasSession: true,
+ getViewAllLabel,
+ advancedLabel: "Advanced search results",
+ addressLabel: "Address search results",
+ defaultLabel: "Search results"
+ })
+ ),
+ {
+ breadcrumbHref: {
+ pathname: "/myportal/viewall",
+ query: { key: "mySubmittedReps" }
+ },
+ caseResultsHref: {
+ pathname: "/myportal/searchresults",
+ query: { va: "true", key: "mySubmittedReps" }
+ },
+ breadcrumbLabel: "Submitted representations"
+ }
+ );
+});
+
+const run = async () => {
+ let passed = 0;
+
+ for (const currentTest of tests) {
+ await currentTest.fn();
+ passed += 1;
+ }
+
+ console.log(
+ `Phase 22 route-state-helper tests passed (${passed}/${tests.length}).`
+ );
+};
+
+module.exports = run;
+
+if (require.main === module) {
+ run().catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
+}