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 @@
- {{ typeof item.name === 'string' && item.name.startsWith('nav.') ? t(item.name) : String(item.name ?? '') }}
+ {{ item.label }}
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 @@
- {{ typeof item.name === 'string' && item.name.startsWith('nav.') ? t(item.name) : String(item.name ?? '') }}
+ {{ item.label }}
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",