From 650c2669457602e3af525eb08323d1759ccb55dc Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 15:03:01 +0530 Subject: [PATCH] feat(renderer): TabBar wired to file store (dirty dot, close, active highlight) - Renders tabs from useFileStore.openTabs with title + dirty dot + close button - Uses Zustand selectors for granular re-renders - Active tab highlighted with aria-current and accent background - Close button stops propagation; does not trigger tab activation - Horizontal scroll on overflow, h-9 fixed bar with border - Empty state preserved when no tabs open Tests: 6 cases (empty, render, highlight, click-switch, click-close, dirty indicator) - 75 total tests (was 70, gained 5 new) - All pass - Build: succeeds Amit Haridas --- src/renderer/components/layout/TabBar.tsx | 57 +++++++++++++- tests/component/layout/TabBar.test.tsx | 91 ++++++++++++++++++++++- 2 files changed, 141 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx index 2500374..29f00e9 100644 --- a/src/renderer/components/layout/TabBar.tsx +++ b/src/renderer/components/layout/TabBar.tsx @@ -1,7 +1,58 @@ +import { X } from 'lucide-react'; +import { useFileStore } from '@/stores/file-store'; +import { cn } from '@/lib/utils'; + export function TabBar() { + const openTabs = useFileStore((s) => s.openTabs); + const activeTabId = useFileStore((s) => s.activeTabId); + const setActiveTab = useFileStore((s) => s.setActiveTab); + const closeTab = useFileStore((s) => s.closeTab); + + if (openTabs.length === 0) { + return ( +
+ No files open +
+ ); + } + return ( -
- No files open +
+
+ {openTabs.map((tab) => { + const isActive = tab.id === activeTabId; + return ( +
setActiveTab(tab.id)} + > + {tab.dirty && ( + + )} + {tab.title} + +
+ ); + })} +
); -} \ No newline at end of file +} diff --git a/tests/component/layout/TabBar.test.tsx b/tests/component/layout/TabBar.test.tsx index 072ef70..94ef08b 100644 --- a/tests/component/layout/TabBar.test.tsx +++ b/tests/component/layout/TabBar.test.tsx @@ -1,10 +1,93 @@ -import { describe, it, expect } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; import { TabBar } from '@/components/layout/TabBar'; +import { useFileStore } from '@/stores/file-store'; describe('TabBar', () => { - it('renders an empty state when no tabs are open', () => { + beforeEach(() => { + useFileStore.setState({ openTabs: [], activeTabId: null }); + }); + + it('renders "No files open" when openTabs is empty', () => { render(); expect(screen.getByText(/no files open/i)).toBeInTheDocument(); }); -}); \ No newline at end of file + + it('renders a tab for each OpenTab', () => { + useFileStore.setState({ + openTabs: [ + { id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: false }, + { id: '/tmp/bar.md', path: '/tmp/bar.md', title: 'bar.md', dirty: false }, + ], + activeTabId: '/tmp/foo.md', + }); + render(); + expect(screen.getByText('foo.md')).toBeInTheDocument(); + expect(screen.getByText('bar.md')).toBeInTheDocument(); + }); + + it('highlights the active tab with aria-current', () => { + useFileStore.setState({ + openTabs: [ + { id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: false }, + { id: '/tmp/bar.md', path: '/tmp/bar.md', title: 'bar.md', dirty: false }, + ], + activeTabId: '/tmp/foo.md', + }); + render(); + const fooTab = screen.getByText('foo.md').closest('[role="tab"]'); + expect(fooTab).toHaveAttribute('aria-current', 'page'); + const barTab = screen.getByText('bar.md').closest('[role="tab"]'); + expect(barTab).not.toHaveAttribute('aria-current'); + }); + + it('clicking a tab calls setActiveTab', async () => { + useFileStore.setState({ + openTabs: [ + { id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: false }, + { id: '/tmp/bar.md', path: '/tmp/bar.md', title: 'bar.md', dirty: false }, + ], + activeTabId: '/tmp/foo.md', + }); + render(); + await act(async () => { + screen.getByText('bar.md').closest('[role="tab"]')!.click(); + }); + expect(useFileStore.getState().activeTabId).toBe('/tmp/bar.md'); + }); + + it('clicking the close button calls closeTab and does not call setActiveTab separately', async () => { + // Use a non-active tab to test that closeTab doesn't change activeTabId + useFileStore.setState({ + openTabs: [ + { id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: false }, + { id: '/tmp/bar.md', path: '/tmp/bar.md', title: 'bar.md', dirty: false }, + ], + activeTabId: '/tmp/bar.md', + }); + render(); + + const fooTab = screen.getByText('foo.md'); + const closeBtn = fooTab.closest('[role="tab"]')!.querySelector('button')!; + await act(async () => { + closeBtn.click(); + }); + + const state = useFileStore.getState(); + expect(state.openTabs.find((t) => t.id === '/tmp/foo.md')).toBeUndefined(); + // activeTabId stays on bar since we closed a non-active tab + expect(state.activeTabId).toBe('/tmp/bar.md'); + }); + + it('renders a dirty indicator when dirty === true', () => { + useFileStore.setState({ + openTabs: [ + { id: '/tmp/foo.md', path: '/tmp/foo.md', title: 'foo.md', dirty: true }, + ], + activeTabId: '/tmp/foo.md', + }); + render(); + const dirtyDot = screen.getByLabelText('Unsaved changes'); + expect(dirtyDot).toBeInTheDocument(); + }); +});