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