docling-studio/frontend/src/shared/breadcrumb/store.test.ts
Pier-Jean Malandrino be51105aaa feat(#208): doc workspace breadcrumb (Studio > <doc> > <mode>)
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
2026-04-29 17:52:59 +02:00

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' }])
})
})