From b6ea858c9b7edc59e440654cfcfc701287fde823 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:11:37 +0200 Subject: [PATCH 1/3] test(10-03): add failing BreadcrumbBar tests (RED) - 10 Vitest unit tests covering showRoot, rootLabel, navigate emits - Tests segments shape {id?, label} (not {id, name}) - Covers ellipsis collapse for >4 segments, no-id static segments - AppIcon stubbed via global.stubs for isolated rendering --- .../ui/__tests__/BreadcrumbBar.test.js | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 frontend/src/components/ui/__tests__/BreadcrumbBar.test.js diff --git a/frontend/src/components/ui/__tests__/BreadcrumbBar.test.js b/frontend/src/components/ui/__tests__/BreadcrumbBar.test.js new file mode 100644 index 0000000..9d9b6be --- /dev/null +++ b/frontend/src/components/ui/__tests__/BreadcrumbBar.test.js @@ -0,0 +1,116 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import BreadcrumbBar from '../BreadcrumbBar.vue' + +const STUBS = { global: { stubs: { AppIcon: true } } } + +function seg(id, label) { return { id, label } } +function staticSeg(label) { return { label } } + +describe('BreadcrumbBar', () => { + it('renders rootLabel button when showRoot=true', () => { + const w = mount(BreadcrumbBar, { + props: { rootLabel: 'Home', segments: [] }, + ...STUBS, + }) + expect(w.find('button').text()).toBe('Home') + }) + + it('does NOT render root button when showRoot=false', () => { + const w = mount(BreadcrumbBar, { + props: { showRoot: false, segments: [staticSeg('Users')] }, + ...STUBS, + }) + expect(w.findAll('button').length).toBe(0) + }) + + it('clicking root button emits navigate(null)', async () => { + const w = mount(BreadcrumbBar, { + props: { segments: [] }, + ...STUBS, + }) + await w.find('button').trigger('click') + expect(w.emitted('navigate')).toBeTruthy() + expect(w.emitted('navigate')[0]).toEqual([null]) + }) + + it('last segment renders as a non-clickable ', () => { + const w = mount(BreadcrumbBar, { + props: { segments: [seg('a', 'A'), seg('b', 'B')] }, + ...STUBS, + }) + const text = w.text() + expect(text).toContain('B') + const buttonWithB = w.findAll('button').filter(b => b.text() === 'B') + expect(buttonWithB.length).toBe(0) + }) + + it('clicking intermediate segment emits navigate(segment.id)', async () => { + const w = mount(BreadcrumbBar, { + props: { segments: [seg('r1', 'Root'), seg('f1', 'Test')] }, + ...STUBS, + }) + const buttons = w.findAll('button') + const rootBtn = buttons.find(b => b.text() === 'Root') + await rootBtn.trigger('click') + expect(w.emitted('navigate')).toBeTruthy() + expect(w.emitted('navigate')[0]).toEqual(['r1']) + }) + + it('segments without id render as plain text even when intermediate', () => { + const w = mount(BreadcrumbBar, { + props: { + showRoot: false, + segments: [staticSeg('Admin'), staticSeg('Users')], + }, + ...STUBS, + }) + expect(w.findAll('button').length).toBe(0) + }) + + it('>4 segments collapse to first + ellipsis + last two', () => { + const segments = [ + seg('a', 'Alpha'), + seg('b', 'Beta'), + seg('c', 'Gamma'), + seg('d', 'Delta'), + seg('e', 'Epsilon'), + ] + const w = mount(BreadcrumbBar, { + props: { segments }, + ...STUBS, + }) + const text = w.text() + expect(text).toContain('Alpha') + expect(text).toContain('…') + expect(text).toContain('Delta') + expect(text).toContain('Epsilon') + expect(text).not.toContain('Beta') + expect(text).not.toContain('Gamma') + }) + + it('rootLabel defaults to "Home"', () => { + const w = mount(BreadcrumbBar, { + props: { segments: [] }, + ...STUBS, + }) + expect(w.find('button').text()).toBe('Home') + }) + + it('custom rootLabel "Cloud" renders correctly', () => { + const w = mount(BreadcrumbBar, { + props: { rootLabel: 'Cloud', segments: [] }, + ...STUBS, + }) + expect(w.find('button').text()).toBe('Cloud') + }) + + it('renders chevronRight separator between segments', () => { + const w = mount(BreadcrumbBar, { + props: { segments: [seg('a', 'A'), seg('b', 'B')] }, + ...STUBS, + }) + const appIcons = w.findAll('app-icon-stub') + expect(appIcons.length).toBeGreaterThanOrEqual(1) + }) +}) From 7e584e032a0341c4dfa4d31d9b8c375678ad3f3e Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:13:29 +0200 Subject: [PATCH 2/3] =?UTF-8?q?feat(10-03):=20implement=20BreadcrumbBar.vu?= =?UTF-8?q?e=20=E2=80=94=20shared=20breadcrumb=20component=20(GREEN)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Options API component with segments/{id?,label}, rootLabel, showRoot props - Emits navigate(segment.id) for intermediate clicks, navigate(null) for root - Computed visibleSegments collapses >4 segments to first+ellipsis+last two - Segments without id render as plain non-clickable spans (admin static segments) - Uses AppIcon for chevronRight separator (no inline SVG) - All 10 Vitest tests pass - Also creates AppIcon.vue (Rule 3: BreadcrumbBar imports it) --- frontend/src/components/ui/AppIcon.vue | 86 ++++++++++++++++++++ frontend/src/components/ui/BreadcrumbBar.vue | 72 ++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 frontend/src/components/ui/AppIcon.vue create mode 100644 frontend/src/components/ui/BreadcrumbBar.vue diff --git a/frontend/src/components/ui/AppIcon.vue b/frontend/src/components/ui/AppIcon.vue new file mode 100644 index 0000000..d27db40 --- /dev/null +++ b/frontend/src/components/ui/AppIcon.vue @@ -0,0 +1,86 @@ + + + diff --git a/frontend/src/components/ui/BreadcrumbBar.vue b/frontend/src/components/ui/BreadcrumbBar.vue new file mode 100644 index 0000000..b580e2f --- /dev/null +++ b/frontend/src/components/ui/BreadcrumbBar.vue @@ -0,0 +1,72 @@ + + + From b4bcc1d8433e770a5dfd1058a7c724536c961cdf Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:14:30 +0200 Subject: [PATCH 3/3] =?UTF-8?q?docs(10-03):=20complete=20BreadcrumbBar=20p?= =?UTF-8?q?lan=20=E2=80=94=202/2=20tasks,=2010/10=20tests=20green?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../phases/10-ux-interaction/10-03-SUMMARY.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .planning/phases/10-ux-interaction/10-03-SUMMARY.md diff --git a/.planning/phases/10-ux-interaction/10-03-SUMMARY.md b/.planning/phases/10-ux-interaction/10-03-SUMMARY.md new file mode 100644 index 0000000..e6aede6 --- /dev/null +++ b/.planning/phases/10-ux-interaction/10-03-SUMMARY.md @@ -0,0 +1,98 @@ +--- +phase: 10-ux-interaction +plan: "03" +subsystem: frontend/ui +tags: [breadcrumb, navigation, vue, tdd, options-api] +dependency_graph: + requires: [] + provides: [BreadcrumbBar.vue, AppIcon.vue] + affects: [StorageBrowser.vue, FileManagerView.vue, CloudFolderView.vue, admin views, settings views] +tech_stack: + added: [] + patterns: [options-api, computed-visibleSegments, AppIcon-separator, tdd-red-green] +key_files: + created: + - frontend/src/components/ui/BreadcrumbBar.vue + - frontend/src/components/ui/AppIcon.vue + - frontend/src/components/ui/__tests__/BreadcrumbBar.test.js + modified: [] +decisions: + - "Options API chosen for BreadcrumbBar per CLAUDE.md convention (despite FolderBreadcrumb using script setup)" + - "AppIcon.vue created in worktree as Rule-3 dependency fix (plan 10-01 creates it in another wave-0 agent)" + - "BreadcrumbBar separator uses AppIcon not inline SVG — icon centralization paradigm enforced" +metrics: + duration: "~5 minutes" + completed: "2026-06-15T18:13:50Z" + tasks_completed: 2 + tasks_total: 2 + files_created: 3 + files_modified: 0 +--- + +# Phase 10 Plan 03: BreadcrumbBar Component Summary + +**One-liner:** Shared breadcrumb component with showRoot/rootLabel props, ellipsis collapse for >4 segments, and static no-id segment support for admin/settings views. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Create BreadcrumbBar.test.js (RED) | b6ea858 | frontend/src/components/ui/__tests__/BreadcrumbBar.test.js | +| 2 | Implement BreadcrumbBar.vue (GREEN) | 7e584e0 | frontend/src/components/ui/BreadcrumbBar.vue, frontend/src/components/ui/AppIcon.vue | + +## What Was Built + +`BreadcrumbBar.vue` is a generalized breadcrumb component that replaces `FolderBreadcrumb.vue` across all views. Key capabilities over the original: + +- **`showRoot` prop** (Boolean, default true): When false, omits the root button entirely — needed for admin/settings views (`Admin > Users`, `Settings > Account`) that have no "Home" concept. +- **`rootLabel` prop** (String, default 'Home'): Configurable root text — file manager uses 'Home', cloud view uses 'Cloud'. +- **Static segments** (no `id`): Segments without an `id` render as non-clickable `` elements. Admin views pass static breadcrumb labels that should not navigate anywhere. +- **`{ id?, label }` shape**: Changed from FolderBreadcrumb's `{ id, name }` to `{ id?, label }`. Wave 1 (plan 10-06) maps existing `{ id, name }` to `{ id, label: name }` at call sites. +- **AppIcon separator**: Uses `` instead of inline SVG — enforces the new icon centralization paradigm from plan 10-01. +- **Ellipsis collapse**: `>4 segments` collapses to `[first, ellipsis, ...last_two]` — carried from FolderBreadcrumb. + +## Test Coverage + +10 Vitest unit tests, all green: +1. Renders rootLabel button when showRoot=true +2. No button rendered when showRoot=false +3. Root click emits navigate(null) +4. Last segment is non-clickable span +5. Intermediate segment click emits navigate(segment.id) +6. No-id segments render as plain text (no buttons) +7. >4 segments collapse to first + ellipsis + last two +8. rootLabel defaults to 'Home' +9. Custom rootLabel 'Cloud' renders correctly +10. chevronRight AppIcon separator present + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] AppIcon.vue missing in worktree** +- **Found during:** Task 2 (implementing BreadcrumbBar.vue which imports AppIcon) +- **Issue:** `frontend/src/components/ui/AppIcon.vue` does not exist in this worktree. Plan 10-01 creates AppIcon in a parallel wave-0 agent. Without AppIcon, the BreadcrumbBar import would fail at test time. +- **Fix:** Created `AppIcon.vue` in the worktree using the full implementation from PATTERNS.md. Identical to what plan 10-01 will produce. When the wave merges, git will show no conflict (same content). +- **Files modified:** `frontend/src/components/ui/AppIcon.vue` (created) +- **Commit:** 7e584e0 + +## Known Stubs + +None. BreadcrumbBar is complete and self-contained. It emits `navigate` events; the caller handles routing. + +## Threat Flags + +None. BreadcrumbBar is a pure presentational component — no network requests, no auth, no user data stored. Segment labels come from trusted store data (folder names, view titles) and are rendered via Vue template interpolation (auto-escaped, no XSS risk). + +## Self-Check: PASSED + +- [x] `frontend/src/components/ui/__tests__/BreadcrumbBar.test.js` exists +- [x] `frontend/src/components/ui/BreadcrumbBar.vue` exists +- [x] `frontend/src/components/ui/AppIcon.vue` exists +- [x] Commit b6ea858 exists (RED phase) +- [x] Commit 7e584e0 exists (GREEN phase) +- [x] All 10 tests pass (`./node_modules/.bin/vitest run BreadcrumbBar` — 10/10) +- [x] `name: 'BreadcrumbBar'` present in BreadcrumbBar.vue +- [x] `rootLabel` appears >= 2 times in BreadcrumbBar.vue +- [x] `showRoot` appears >= 3 times in BreadcrumbBar.vue +- [x] `