import { describe, it, expect, vi, beforeEach } from 'vitest' import { act, render, screen, fireEvent } from '@testing-library/react' import { StatusBar } from './StatusBar' import type { VaultOption } from './StatusBar' 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' }, ] const installedAiAgentsStatus = { claude_code: { status: 'installed' as const, version: '1.0.20' }, codex: { status: 'installed' as const, version: '0.37.0' }, } describe('StatusBar', () => { beforeEach(() => { vi.clearAllMocks() }) it('does not display the bottom-bar note count readout', () => { render() expect(screen.queryByText('9,200 notes')).not.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 Feedback button when callback is provided', () => { render() expect(screen.getByTestId('status-feedback')).toBeInTheDocument() expect(screen.getByText('Feedback')).toBeInTheDocument() }) it('calls onOpenFeedback when Feedback is clicked', () => { const onOpenFeedback = vi.fn() render() fireEvent.click(screen.getByTestId('status-feedback')) expect(onOpenFeedback).toHaveBeenCalledOnce() }) 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('Clone Git repo')).toBeInTheDocument() }) it('shows the Getting Started clone action in the vault menu when provided', () => { render( ) fireEvent.click(screen.getByTitle('Switch vault')) expect(screen.getByText('Clone Getting Started Vault')).toBeInTheDocument() }) it('calls onCloneGettingStarted when clicking the vault menu action', () => { const onCloneGettingStarted = vi.fn() render( ) fireEvent.click(screen.getByTitle('Switch vault')) fireEvent.click(screen.getByText('Clone Getting Started Vault')) expect(onCloneGettingStarted).toHaveBeenCalledOnce() }) it('shows Changes badge with count when modifiedCount is > 0', () => { render() expect(screen.getByTestId('status-modified-count')).toBeInTheDocument() expect(screen.getByText('Changes')).toBeInTheDocument() expect(screen.getByText('3')).toBeInTheDocument() }) it('does not show Changes badge when modifiedCount is 0', () => { render() expect(screen.queryByTestId('status-modified-count')).not.toBeInTheDocument() }) it('does not show Changes badge 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 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 Pull required label when syncStatus is pull_required', () => { render( ) expect(screen.getByText('Pull required')).toBeInTheDocument() }) it('shows an offline chip when offline', () => { render( ) expect(screen.getByTestId('status-offline')).toHaveTextContent('Offline') }) it('shows a no-remote chip when the active git vault has no remote', () => { render( ) expect(screen.getByTestId('status-no-remote')).toHaveTextContent('No remote') }) it('calls onPullAndPush when clicking Pull required badge', () => { const onPullAndPush = vi.fn() render( ) fireEvent.click(screen.getByTestId('status-sync')) expect(onPullAndPush).toHaveBeenCalledOnce() }) it('shows git status popup when clicking idle sync badge', () => { render( ) fireEvent.click(screen.getByTestId('status-sync')) expect(screen.getByTestId('git-status-popup')).toBeInTheDocument() expect(screen.getByText('main')).toBeInTheDocument() expect(screen.getByText(/2 ahead/)).toBeInTheDocument() expect(screen.getByText(/1 behind/)).toBeInTheDocument() }) it('shows History badge in status bar', () => { render() expect(screen.getByTestId('status-pulse')).toBeInTheDocument() expect(screen.getByText('History')).toBeInTheDocument() }) it('calls onClickPulse when clicking History badge', () => { const onClickPulse = vi.fn() render() fireEvent.click(screen.getByTestId('status-pulse')) expect(onClickPulse).toHaveBeenCalledOnce() }) it('disables History badge when isGitVault is false', () => { const onClickPulse = vi.fn() render() fireEvent.click(screen.getByTestId('status-pulse')) expect(onClickPulse).not.toHaveBeenCalled() }) it('shows Commit button in status bar', () => { const onCommitPush = vi.fn() render() expect(screen.getByTestId('status-commit-push')).toBeInTheDocument() fireEvent.click(screen.getByTestId('status-commit-push')) expect(onCommitPush).toHaveBeenCalledOnce() }) it('uses a local-only tooltip for the commit button when no remote is configured', () => { render( ) expect(screen.getByTestId('status-commit-push')).toHaveAttribute('title', 'Commit locally (no remote configured)') }) it('shows Commit button even when no modified files', () => { render() expect(screen.getByTestId('status-commit-push')).toBeInTheDocument() }) it('hides Commit button when no onCommitPush callback', () => { render() expect(screen.queryByTestId('status-commit-push')).not.toBeInTheDocument() }) it('shows Claude Code badge when installed', () => { render() const badge = screen.getByTestId('status-claude-code') expect(badge).toBeInTheDocument() expect(screen.getByText('Claude Code')).toBeInTheDocument() expect(badge.getAttribute('title')).toBe('Claude Code 1.0.20') }) it('shows Claude Code missing badge with warning when missing', () => { render() const badge = screen.getByTestId('status-claude-code') expect(badge).toBeInTheDocument() expect(screen.getByText('Claude Code missing')).toBeInTheDocument() expect(badge.getAttribute('title')).toBe('Claude Code not found — click to install') }) it('opens install URL when clicking missing Claude Code badge', () => { render() fireEvent.click(screen.getByTestId('status-claude-code')) expect(openExternalUrl).toHaveBeenCalledWith('https://docs.anthropic.com/en/docs/claude-code') }) it('opens install URL on Enter key for missing Claude Code badge', () => { render() fireEvent.keyDown(screen.getByTestId('status-claude-code'), { key: 'Enter' }) expect(openExternalUrl).toHaveBeenCalledWith('https://docs.anthropic.com/en/docs/claude-code') }) it('hides Claude Code badge when status is checking', () => { render() expect(screen.queryByTestId('status-claude-code')).not.toBeInTheDocument() }) it('hides Claude Code badge when no claudeCodeStatus prop provided', () => { render() expect(screen.queryByTestId('status-claude-code')).not.toBeInTheDocument() }) it('shows the active AI agent directly in the bottom bar', () => { render( , ) expect(screen.getByTestId('status-ai-agents')).toHaveTextContent('AI: Claude') }) it('opens the AI agent switcher from the keyboard and switches agents', () => { const onSetDefaultAiAgent = vi.fn() render( , ) const trigger = screen.getByTestId('status-ai-agents') trigger.focus() act(() => { fireEvent.keyDown(trigger, { key: 'ArrowDown' }) }) const codexOption = screen.getByRole('menuitemradio', { name: /Codex/ }) codexOption.focus() act(() => { fireEvent.keyDown(codexOption, { key: 'Enter' }) }) expect(onSetDefaultAiAgent).toHaveBeenCalledWith('codex') }) })