diff --git a/components/breadcrumbs.js b/components/breadcrumbs.js index 90c2a391..591224b2 100644 --- a/components/breadcrumbs.js +++ b/components/breadcrumbs.js @@ -13,6 +13,10 @@ import { setRepresentationSubmit } from "../store/currentView/action"; import { resolveCaseBreadcrumbState } from "../lib/routing/routeState"; +import { + buildBreadcrumbRendererMaps, + resolveMappedRouteRenderer as resolveMappedRouteRendererByMaps +} from "../lib/routing/breadcrumbRouteMaps"; const Breadcrumbs = (props) => { const { currentView, @@ -206,27 +210,14 @@ const Breadcrumbs = (props) => { ) }; - const mappedRouteRendererMaps = [ + const mappedRouteRendererMaps = buildBreadcrumbRendererMaps({ simpleRouteRenderersByPath, simpleLinkTextPairRenderersByPath, simpleMyPortalRouteRenderersByPath - ]; - - const getMappedRendererByPath = (path, routeRendererMap) => - routeRendererMap[path] || null; + }); const resolveMappedRouteRenderer = (path) => { - for (const routeRendererMap of mappedRouteRendererMaps) { - const routeRenderer = getMappedRendererByPath( - path, - routeRendererMap - ); - if (routeRenderer) { - return routeRenderer; - } - } - - return null; + return resolveMappedRouteRendererByMaps(path, mappedRouteRendererMaps); }; const renderMappedRoute = (path, resolver = resolveMappedRouteRenderer) => { diff --git a/lib/routing/breadcrumbRouteMaps.js b/lib/routing/breadcrumbRouteMaps.js new file mode 100644 index 00000000..cf5038bc --- /dev/null +++ b/lib/routing/breadcrumbRouteMaps.js @@ -0,0 +1,29 @@ +const getMappedRendererByPath = (path, routeRendererMap) => + routeRendererMap[path] || 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 +}) => [ + simpleRouteRenderersByPath, + simpleLinkTextPairRenderersByPath, + simpleMyPortalRouteRenderersByPath +]; + +module.exports = { + buildBreadcrumbRendererMaps, + getMappedRendererByPath, + resolveMappedRouteRenderer +}; diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index f2e0d2c0..272f9802 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -492,6 +492,46 @@ 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-00X: 22500 `components/elements/index.js` Phase 1 helper extraction date: 2026-04-07 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..211be03a --- /dev/null +++ b/tests/phase22/breadcrumb-route-maps-helper.test.cjs @@ -0,0 +1,69 @@ +const assert = require("assert"); + +const { + buildBreadcrumbRendererMaps, + getMappedRendererByPath, + resolveMappedRouteRenderer +} = require("../../lib/routing/breadcrumbRouteMaps"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("breadcrumbRouteMaps/buildBreadcrumbRendererMaps preserves explicit map order", async () => { + const mapA = { "/a": () => "A" }; + const mapB = { "/b": () => "B" }; + const mapC = { "/c": () => "C" }; + + const result = buildBreadcrumbRendererMaps({ + simpleRouteRenderersByPath: mapA, + simpleLinkTextPairRenderersByPath: mapB, + simpleMyPortalRouteRenderersByPath: mapC + }); + + assert.strictEqual(result[0], mapA); + assert.strictEqual(result[1], mapB); + assert.strictEqual(result[2], mapC); +}); + +test("breadcrumbRouteMaps/getMappedRendererByPath returns mapped renderer or null", async () => { + const renderer = () => "ok"; + const map = { "/x": renderer }; + + assert.strictEqual(getMappedRendererByPath("/x", map), renderer); + assert.strictEqual(getMappedRendererByPath("/missing", map), null); +}); + +test("breadcrumbRouteMaps/resolveMappedRouteRenderer honors map precedence", async () => { + const firstRenderer = () => "first"; + const secondRenderer = () => "second"; + + const result = resolveMappedRouteRenderer("/same", [ + { "/same": firstRenderer }, + { "/same": secondRenderer } + ]); + + assert.strictEqual(result, firstRenderer); + assert.strictEqual(resolveMappedRouteRenderer("/none", []), null); +}); + +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 index 77c0e4a7..51b3512d 100644 --- a/tests/phase22/breadcrumbs-route-map-structure.test.cjs +++ b/tests/phase22/breadcrumbs-route-map-structure.test.cjs @@ -60,17 +60,25 @@ test("breadcrumbs/merged resolver renders mapped routes through single helper in const source = loadBreadcrumbSource(); assert.strictEqual( - source.includes("const mappedRouteRendererMaps = ["), + source.includes('from "../lib/routing/breadcrumbRouteMaps"'), true, - "Expected grouped mapped route renderer map composition to exist" + "Expected breadcrumbs to import shared breadcrumb route-map helper module" ); assert.strictEqual( source.includes( - "const getMappedRendererByPath = (path, routeRendererMap) =>" + "const mappedRouteRendererMaps = buildBreadcrumbRendererMaps({" ), true, - "Expected shared mapped renderer lookup helper to exist" + "Expected grouped mapped route renderer map composition to use helper builder" + ); + + assert.strictEqual( + source.includes( + "return resolveMappedRouteRendererByMaps(path, mappedRouteRendererMaps);" + ), + true, + "Expected resolver to delegate to shared route-map resolver helper" ); assert.strictEqual( @@ -104,6 +112,32 @@ test("breadcrumbs/merged resolver renders mapped routes through single helper in false, "Expected /myportal/addresssearchresults explicit branch to be removed after mapping" ); + + const mapBuilderCallIndex = source.indexOf( + "const mappedRouteRendererMaps = buildBreadcrumbRendererMaps({" + ); + const mapBuilderSlice = source.slice( + mapBuilderCallIndex, + mapBuilderCallIndex + 300 + ); + + const simpleRouteOrderIndex = mapBuilderSlice.indexOf( + "simpleRouteRenderersByPath" + ); + const simpleLinkTextPairOrderIndex = mapBuilderSlice.indexOf( + "simpleLinkTextPairRenderersByPath" + ); + const simpleMyPortalOrderIndex = mapBuilderSlice.indexOf( + "simpleMyPortalRouteRenderersByPath" + ); + + assert.strictEqual( + simpleRouteOrderIndex >= 0 && + simpleLinkTextPairOrderIndex > simpleRouteOrderIndex && + simpleMyPortalOrderIndex > simpleLinkTextPairOrderIndex, + true, + "Expected map builder call to preserve explicit route-map ordering (simple -> link-text -> myportal)" + ); }); test("breadcrumbs/dynamic callback branch remains explicit for myportal case incident route", async () => { diff --git a/tests/phase22/index.test.cjs b/tests/phase22/index.test.cjs index b3996055..7bfabdc5 100644 --- a/tests/phase22/index.test.cjs +++ b/tests/phase22/index.test.cjs @@ -7,6 +7,7 @@ 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 () => { @@ -19,6 +20,7 @@ const run = async () => { await runI18nRouteParityTests(); await runAzurestorageHelperTests(); await runRouteStateHelperTests(); + await runBreadcrumbRouteMapsHelperTests(); await runBreadcrumbsRouteMapStructureTests(); console.log("Phase 22 combined suite passed."); };