Adds the breadcrumb anchoring the user across modes on the doc workspace. Empty / hidden on routes that don't opt in. Components - shared/breadcrumb/AppBreadcrumb.vue: data-driven, accessible (<nav aria-label>, <ol>, aria-current=page on the leaf). Renders nothing when crumbs.length === 0. - shared/breadcrumb/types.ts: Crumb = LinkCrumb | LeafCrumb discriminated union. - shared/breadcrumb/store.ts: Pinia store + useCrumbs(source) composable that auto-clears on unmount. Accepts a static array OR a reactive ref/computed so pages with async doc fetches can rebuild crumbs as data lands. - shared/breadcrumb/text.ts: truncate(text, max) helper. Wiring - App.vue main outlet now sits below <AppBreadcrumb>; the shell reads from useBreadcrumbStore so pages don't need teleports. - DocWorkspacePage provides Studio > <id-truncated> > <mode-label>; once the doc is fetched (#216 / E4), the id placeholder will swap for the truncated filename. i18n - breadcrumb.aria, breadcrumb.studio, breadcrumb.mode.{ask,inspect, chunks} added in fr + en. Tests - shared/breadcrumb/text.test.ts: 5 cases on truncate. - shared/breadcrumb/store.test.ts: store actions + useCrumbs reactive/ static seeding (the lifecycle-clear path is covered end-to-end by the integration tests landing in #211 / #216 — the project doesn't use @vue/test-utils so a unit test for onBeforeUnmount is overkill). Refs #208
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
import { createPinia, setActivePinia } from 'pinia'
|
|
import { ref, type Ref } from 'vue'
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { useBreadcrumbStore, useCrumbs } from './store'
|
|
import type { Crumb } from './types'
|
|
|
|
/**
|
|
* The composable side of `useCrumbs` registers `onBeforeUnmount` which
|
|
* requires a component lifecycle context. The project does not have
|
|
* `@vue/test-utils`, so we test the store API directly. The composable
|
|
* lifecycle is exercised end-to-end by the integration tests landing
|
|
* in #211 / #216.
|
|
*
|
|
* We DO test the reactive-input behaviour by calling `useCrumbs` with
|
|
* a ref inside a setup-style scope; the watch fires synchronously
|
|
* thanks to `immediate: true`.
|
|
*/
|
|
describe('useBreadcrumbStore', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
})
|
|
|
|
it('starts empty', () => {
|
|
const store = useBreadcrumbStore()
|
|
expect(store.crumbs).toEqual([])
|
|
})
|
|
|
|
it('setCrumbs replaces the segments', () => {
|
|
const store = useBreadcrumbStore()
|
|
const next: Crumb[] = [{ kind: 'leaf', label: 'a' }]
|
|
store.setCrumbs(next)
|
|
expect(store.crumbs).toEqual(next)
|
|
})
|
|
|
|
it('clear empties the segments', () => {
|
|
const store = useBreadcrumbStore()
|
|
store.setCrumbs([{ kind: 'leaf', label: 'a' }])
|
|
store.clear()
|
|
expect(store.crumbs).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('useCrumbs (reactive source path)', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
})
|
|
|
|
it('seeds the store from a ref synchronously', () => {
|
|
const store = useBreadcrumbStore()
|
|
const dynamic: Ref<Crumb[]> = ref([{ kind: 'leaf', label: 'first' }])
|
|
// Note: `useCrumbs` calls `onBeforeUnmount` which is a no-op when
|
|
// there is no current component — Vue logs a warning but the watch
|
|
// path still installs.
|
|
useCrumbs(dynamic)
|
|
expect(store.crumbs).toEqual([{ kind: 'leaf', label: 'first' }])
|
|
|
|
dynamic.value = [{ kind: 'leaf', label: 'second' }]
|
|
// The watch is `immediate: true`; for subsequent updates it runs
|
|
// on the next microtask.
|
|
return Promise.resolve().then(() => {
|
|
expect(store.crumbs).toEqual([{ kind: 'leaf', label: 'second' }])
|
|
})
|
|
})
|
|
|
|
it('seeds the store from a static array', () => {
|
|
const store = useBreadcrumbStore()
|
|
useCrumbs([{ kind: 'leaf', label: 'static' }])
|
|
expect(store.crumbs).toEqual([{ kind: 'leaf', label: 'static' }])
|
|
})
|
|
})
|