import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import { StatusBar } from './StatusBar' import type { VaultOption } from './StatusBar' import { formatIndexedElapsed } from '../utils/indexingHelpers' vi.mock('../utils/url', async () => { const actual = await vi.importActual('../utils/url') return { ...actual, openExternalUrl: vi.fn().mockResolvedValue(undefined) } }) const { openExternalUrl } = await import('../utils/url') as typeof import('../utils/url') & { openExternalUrl: ReturnType } const vaults: VaultOption[] = [ { label: 'Main Vault', path: '/Users/luca/Laputa' }, { label: 'Work Vault', path: '/Users/luca/Work' }, ] describe('StatusBar', () => { beforeEach(() => { vi.clearAllMocks() }) it('displays note count', () => { render() expect(screen.getByText('9,200 notes')).toBeInTheDocument() }) it('displays build number when provided', () => { render() expect(screen.getByText('b223')).toBeInTheDocument() }) it('displays fallback build number when not provided', () => { render() expect(screen.getByText('b?')).toBeInTheDocument() }) it('calls onCheckForUpdates when clicking build number', () => { const onCheckForUpdates = vi.fn() render() fireEvent.click(screen.getByTestId('status-build-number')) expect(onCheckForUpdates).toHaveBeenCalledOnce() }) it('build number has "Check for updates" title', () => { render() expect(screen.getByTitle('Check for updates')).toBeInTheDocument() }) it('does not display branch name', () => { render() expect(screen.queryByText('main')).not.toBeInTheDocument() }) it('shows clickable commit hash that opens URL via openExternalUrl', () => { render( ) const link = screen.getByTestId('status-commit-link') expect(link).toBeInTheDocument() expect(link.tagName).toBe('SPAN') expect(screen.getByText('a3f9b1c')).toBeInTheDocument() fireEvent.click(link) expect(openExternalUrl).toHaveBeenCalledWith('https://github.com/owner/repo/commit/abc123') }) it('shows non-clickable commit hash when no commitUrl', () => { render( ) const span = screen.getByTestId('status-commit-hash') expect(span).toBeInTheDocument() expect(span.tagName).toBe('SPAN') expect(screen.getByText('b4e2d8f')).toBeInTheDocument() }) it('hides commit hash when lastCommitInfo is null', () => { render( ) expect(screen.queryByTestId('status-commit-link')).not.toBeInTheDocument() expect(screen.queryByTestId('status-commit-hash')).not.toBeInTheDocument() }) it('displays active vault name', () => { render() expect(screen.getByText('Main Vault')).toBeInTheDocument() }) it('shows fallback "Vault" when vault path does not match', () => { render() expect(screen.getByText('Vault')).toBeInTheDocument() }) it('opens vault menu on click and shows all vault options', () => { render() // Click the vault button to open menu fireEvent.click(screen.getByTitle('Switch vault')) expect(screen.getByText('Work Vault')).toBeInTheDocument() }) it('calls onSwitchVault when selecting a different vault', () => { const onSwitchVault = vi.fn() render() fireEvent.click(screen.getByTitle('Switch vault')) // Click "Work Vault" fireEvent.click(screen.getByText('Work Vault')) expect(onSwitchVault).toHaveBeenCalledWith('/Users/luca/Work') }) it('closes vault menu when clicking outside', () => { render() fireEvent.click(screen.getByTitle('Switch vault')) expect(screen.getByText('Work Vault')).toBeInTheDocument() // Click outside the menu fireEvent.mouseDown(document.body) expect(screen.queryByText('Work Vault')).not.toBeInTheDocument() }) it('toggles vault menu open and closed', () => { render() const vaultButton = screen.getByTitle('Switch vault') fireEvent.click(vaultButton) expect(screen.getByText('Work Vault')).toBeInTheDocument() // Click again to close fireEvent.click(vaultButton) expect(screen.queryByText('Work Vault')).not.toBeInTheDocument() }) it('shows "Open local folder" option in vault menu', () => { render( ) fireEvent.click(screen.getByTitle('Switch vault')) expect(screen.getByText('Open local folder')).toBeInTheDocument() }) it('calls onOpenLocalFolder when clicking "Open local folder"', () => { const onOpenLocalFolder = vi.fn() render( ) fireEvent.click(screen.getByTitle('Switch vault')) fireEvent.click(screen.getByText('Open local folder')) expect(onOpenLocalFolder).toHaveBeenCalledOnce() }) it('shows add-vault options in vault menu', () => { render( ) fireEvent.click(screen.getByTitle('Switch vault')) expect(screen.getByText('Open local folder')).toBeInTheDocument() expect(screen.getByText('Connect GitHub repo')).toBeInTheDocument() }) it('shows modified count when modifiedCount is > 0', () => { render() expect(screen.getByTestId('status-modified-count')).toBeInTheDocument() expect(screen.getByText('3 pending')).toBeInTheDocument() }) it('does not show modified count when modifiedCount is 0', () => { render() expect(screen.queryByTestId('status-modified-count')).not.toBeInTheDocument() }) it('does not show modified count when modifiedCount is not provided', () => { render() expect(screen.queryByTestId('status-modified-count')).not.toBeInTheDocument() }) it('closes menu after clicking "Open local folder"', () => { render( ) fireEvent.click(screen.getByTitle('Switch vault')) fireEvent.click(screen.getByText('Open local folder')) // Menu should close after clicking an action expect(screen.queryByText('Open local folder')).not.toBeInTheDocument() }) it('calls onClickPending when clicking the pending count', () => { const onClickPending = vi.fn() render( ) fireEvent.click(screen.getByTestId('status-modified-count')) expect(onClickPending).toHaveBeenCalledOnce() }) it('pending count has title for accessibility', () => { render( ) expect(screen.getByTitle('View pending changes')).toBeInTheDocument() }) it('shows indexing badge when indexing is in progress', () => { render( ) expect(screen.getByTestId('status-indexing')).toBeInTheDocument() expect(screen.getByText(/Indexing… 342\/1,057/)).toBeInTheDocument() }) it('shows embedding phase in indexing badge', () => { render( ) expect(screen.getByText(/Embedding… 50\/200/)).toBeInTheDocument() }) it('shows index ready when indexing is complete', () => { render( ) expect(screen.getByText('Index ready')).toBeInTheDocument() }) it('shows error state in indexing badge with retry label', () => { render( ) expect(screen.getByText('Index failed — retry')).toBeInTheDocument() }) it('hides indexing badge when phase is unavailable', () => { render( ) expect(screen.queryByTestId('status-indexing')).not.toBeInTheDocument() }) it('calls onRetryIndexing when clicking error badge', () => { const onRetryIndexing = vi.fn() render( ) fireEvent.click(screen.getByTestId('status-indexing')) expect(onRetryIndexing).toHaveBeenCalledOnce() }) it('hides indexing badge when phase is idle', () => { render( ) expect(screen.queryByTestId('status-indexing')).not.toBeInTheDocument() }) it('hides indexing badge when no progress prop provided', () => { render( ) expect(screen.queryByTestId('status-indexing')).not.toBeInTheDocument() }) it('shows installing phase in indexing badge', () => { render( ) expect(screen.getByText('Installing search…')).toBeInTheDocument() }) it('shows MCP warning badge when status is not_installed', () => { render( ) expect(screen.getByTestId('status-mcp')).toBeInTheDocument() expect(screen.getByTitle('MCP server not installed — click to install')).toBeInTheDocument() }) it('shows MCP warning badge when status is no_claude_cli', () => { render( ) expect(screen.getByTestId('status-mcp')).toBeInTheDocument() expect(screen.getByTitle('Claude CLI not found — install it first')).toBeInTheDocument() }) it('hides MCP badge when status is installed', () => { render( ) expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument() }) it('hides MCP badge when status is checking', () => { render( ) expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument() }) it('hides MCP badge when no mcpStatus prop provided', () => { render( ) expect(screen.queryByTestId('status-mcp')).not.toBeInTheDocument() }) it('calls onInstallMcp when clicking MCP badge with not_installed status', () => { const onInstallMcp = vi.fn() render( ) fireEvent.click(screen.getByTestId('status-mcp')) expect(onInstallMcp).toHaveBeenCalledOnce() }) it('does not call onInstallMcp when clicking MCP badge with no_claude_cli status', () => { const onInstallMcp = vi.fn() render( ) fireEvent.click(screen.getByTestId('status-mcp')) expect(onInstallMcp).not.toHaveBeenCalled() }) it('shows "Indexed just now" when lastIndexedTime is recent and phase is idle', () => { render( ) expect(screen.getByText(/Indexed just now/)).toBeInTheDocument() expect(screen.getByTestId('status-indexed-time')).toBeInTheDocument() }) it('calls onReindexVault when clicking the indexed time badge', () => { const onReindexVault = vi.fn() render( ) fireEvent.click(screen.getByTestId('status-indexed-time')) expect(onReindexVault).toHaveBeenCalledOnce() }) it('hides indexed time badge when no lastIndexedTime', () => { render( ) expect(screen.queryByTestId('status-indexed-time')).not.toBeInTheDocument() }) }) describe('formatIndexedElapsed', () => { it('returns empty string for null', () => { expect(formatIndexedElapsed(null)).toBe('') }) it('returns "Indexed just now" for < 60s', () => { expect(formatIndexedElapsed(Date.now() - 30_000)).toBe('Indexed just now') }) it('returns minutes for < 60min', () => { expect(formatIndexedElapsed(Date.now() - 5 * 60_000)).toBe('Indexed 5m ago') }) it('returns hours for < 24h', () => { expect(formatIndexedElapsed(Date.now() - 3 * 3600_000)).toBe('Indexed 3h ago') }) it('returns days for >= 24h', () => { expect(formatIndexedElapsed(Date.now() - 48 * 3600_000)).toBe('Indexed 2d ago') }) })