Merged PR 2231: refactor breadcrumb

Related work items: #22541
This commit is contained in:
Robert Bond
2026-04-09 15:36:06 +00:00
16 changed files with 3252 additions and 974 deletions
+2 -1
View File
@@ -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
+1
View File
@@ -59,3 +59,4 @@ GUARDRAILS.md
ai-prompts/
workflows/
AI_CONTEXT.md
pages/baracuda.min.html
+203 -913
View File
File diff suppressed because it is too large Load Diff
+13 -49
View File
@@ -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) => {
<Link
className="govuk-button govuk-button--secondary"
href={buildSearchHref(
router,
session
)}
href={searchHref}
>
{" "}
{t(
+7 -4
View File
@@ -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,
+1 -1
View File
@@ -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"],
+618
View File
@@ -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"))}
<li className="govuk-breadcrumbs__list-item">
{t("account:account-title")}
</li>
</>
),
"/account/changepassword": () => (
<>
{renderLinkCrumb(cyMyPortalHref, t("common:breadcrumb-my-portal"))}
<li className="govuk-breadcrumbs__list-item">
Update your password
</li>
</>
)
});
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
};
};
+47
View File
@@ -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
};
+133
View File
@@ -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
})
};
};
+894
View File
@@ -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 `<li>` 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 `<li>` is not nested in another `<li>`).
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 `<li><Link ...>` (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 `<li><Link ...>` 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 (`<a href="#">` and occasional `<a href={...}>`) 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 `<a ...>` 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 `<li><Link ...>` 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
+37
View File
@@ -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`
+6 -6
View File
@@ -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 () => {
@@ -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);
});
}
@@ -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);
});
}
+6
View File
@@ -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.");
};
+392
View File
@@ -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);
});
}