diff --git a/apps/frontend/src/components/AppBreadcrumb.vue b/apps/frontend/src/components/AppBreadcrumb.vue index 2a375e449..729622cc0 100644 --- a/apps/frontend/src/components/AppBreadcrumb.vue +++ b/apps/frontend/src/components/AppBreadcrumb.vue @@ -1,44 +1,69 @@ diff --git a/apps/frontend/src/data/canaSchema.ts b/apps/frontend/src/data/canaSchema.ts index 8fddf9d50..64315ae8f 100644 --- a/apps/frontend/src/data/canaSchema.ts +++ b/apps/frontend/src/data/canaSchema.ts @@ -32,7 +32,7 @@ const storeNameFromListPath = (operationId: string, schemaName: string): string for (const [path, methods] of Object.entries(document.paths ?? {})) { for (const operation of Object.values(methods)) { if (operation?.operationId === operationId) { - const segment = path.split('/').filter(Boolean).findLast(Boolean); + const segment = path.split('/').filter(Boolean).at(-1); if (segment) return segment; } } diff --git a/apps/frontend/test/component/AppBreadcrumb.test.ts b/apps/frontend/test/component/AppBreadcrumb.test.ts new file mode 100644 index 000000000..1eff48709 --- /dev/null +++ b/apps/frontend/test/component/AppBreadcrumb.test.ts @@ -0,0 +1,64 @@ +import { + afterEach, describe, expect, it +} from 'bun:test'; +import { createMemoryHistory, createRouter } from 'vue-router'; + +import AppBreadcrumb from '@/components/AppBreadcrumb.vue'; +import '@/modules/index'; + +import { flush, freshSession, mountWithShell } from './support'; + +/** + * JUM-906: the breadcrumb read `route.matched[].path`, which is the route + * pattern — a module page showed "Home / Home" and linked to + * `/#/m/:moduleId/:tab?`. + */ +const routes = [ + { + path: '/', + name: 'Home', + component: { template: '' }, + meta: { titleKey: 'nav.home' }, + children: [ + { + path: '/m/:moduleId/:tab?', name: 'Module', component: { template: '
' }, meta: { titleKey: 'nav.home' } + } + ] + } +]; + +async function crumbsAt(path: string) { + const pinia = freshSession(); + const router = createRouter({ history: createMemoryHistory(), routes }); + await router.push(path); + const wrapper = mountWithShell(AppBreadcrumb, { pinia, global: { plugins: [pinia, router] } }); + await flush(2); + const items = wrapper.findAll('.breadcrumb-item').map((item) => ({ + text: item.text(), + href: item.find('a').exists() ? item.find('a').attributes('href') : '' + })); + wrapper.unmount(); + return items; +} + +describe('AppBreadcrumb (JUM-906)', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('names the module and the active tab instead of repeating Home', async () => { + expect.hasAssertions(); + const items = await crumbsAt('/m/users/dashboard'); + + expect(items.map((item) => item.text)).toStrictEqual(['Home', 'Users', 'Dashboard']); + }); + + it('links to resolved locations, never to a route pattern', async () => { + expect.hasAssertions(); + const items = await crumbsAt('/m/users/organizations'); + + expect(items.map((item) => item.text)).toStrictEqual(['Home', 'Users', 'Organizations']); + expect(items.some((item) => item.href.includes(':moduleId'))).toBe(false); + expect(items[1].href).toBe('/m/users'); + }); +}); diff --git a/packages/cli-init/templates.manifest.json b/packages/cli-init/templates.manifest.json index 999495701..e7e82fd78 100644 --- a/packages/cli-init/templates.manifest.json +++ b/packages/cli-init/templates.manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceCommit": "d038d9ebf8b2d0e234ea7c12c6336f356d4b1595", + "sourceCommit": "2a9a981e16853b7596716e505844f22611d2b506", "exclusions": [ "**/.agents/**", "**/.claude/**", @@ -3788,7 +3788,7 @@ "source": "apps/frontend/src/App.vue" }, "frontend/src/components/AppBreadcrumb.vue": { - "sha256": "6f3b663e86a6e799b1e424336424f0bf81101e2d6e9c528c39c0e464d910244c", + "sha256": "f47862797e201e7c32a01047baa06c52e6574a32e06c824e7924867c8d9b9dff", "source": "apps/frontend/src/components/AppBreadcrumb.vue" }, "frontend/src/components/AppFooter.vue": { @@ -3972,7 +3972,7 @@ "source": "apps/frontend/src/contracts/validation.ts" }, "frontend/src/data/canaSchema.ts": { - "sha256": "358e8e3e26e9b6266b89d7ef568941018d4d5ba19255827cf153cc61605e05d0", + "sha256": "1d34048e3b810346d445ea334fb03ac7391521c4fe6149391e278cfddd37b2d9", "source": "apps/frontend/src/data/canaSchema.ts" }, "frontend/src/data/db.ts": { diff --git a/packages/cli-init/templates/frontend/src/components/AppBreadcrumb.vue b/packages/cli-init/templates/frontend/src/components/AppBreadcrumb.vue index 2a375e449..729622cc0 100644 --- a/packages/cli-init/templates/frontend/src/components/AppBreadcrumb.vue +++ b/packages/cli-init/templates/frontend/src/components/AppBreadcrumb.vue @@ -1,44 +1,69 @@ diff --git a/packages/cli-init/templates/frontend/src/data/canaSchema.ts b/packages/cli-init/templates/frontend/src/data/canaSchema.ts index 8fddf9d50..64315ae8f 100644 --- a/packages/cli-init/templates/frontend/src/data/canaSchema.ts +++ b/packages/cli-init/templates/frontend/src/data/canaSchema.ts @@ -32,7 +32,7 @@ const storeNameFromListPath = (operationId: string, schemaName: string): string for (const [path, methods] of Object.entries(document.paths ?? {})) { for (const operation of Object.values(methods)) { if (operation?.operationId === operationId) { - const segment = path.split('/').filter(Boolean).findLast(Boolean); + const segment = path.split('/').filter(Boolean).at(-1); if (segment) return segment; } } diff --git a/test-map.json b/test-map.json index 8cfe8e5b6..15ee23fbc 100644 --- a/test-map.json +++ b/test-map.json @@ -2661,6 +2661,18 @@ "tier": "gate", "timeoutMs": 60000 }, + { + "id": "apps/frontend/test/component/AppBreadcrumb.test.ts", + "path": "apps/frontend/test/component/AppBreadcrumb.test.ts", + "layer": "frontend", + "kind": "non-hexagonal", + "type": "unit", + "runner": "bun", + "script": "frontend:test:unit", + "reason": "bun:test suites mounting shipped .vue components through @vue/test-utils + happy-dom (JUM-776); the app-scoped bunfig preload registers the SFC loader.", + "tier": "gate", + "timeoutMs": 60000 + }, { "id": "apps/frontend/test/component/ModuleLayout.test.ts", "path": "apps/frontend/test/component/ModuleLayout.test.ts",