// @vitest-environment jsdom import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import { Sidebar } from './Sidebar' // The preload bridge is read once at module load, so the switches need it mocked rather than // assigned onto `window` after the fact. const bridge = vi.hoisted(() => ({ companionStatus: vi.fn(), openExternal: vi.fn(), })) vi.mock('../lib/ipc', () => ({ codeburn: bridge, normalizeCliError: (err: unknown) => err })) // This jsdom setup ships no Storage, and the sidebar's collapsed state is read // from one at first render. const store = new Map() vi.stubGlobal('localStorage', { getItem: (key: string) => store.get(key) ?? null, setItem: (key: string, value: string) => { store.set(key, value) }, removeItem: (key: string) => { store.delete(key) }, clear: () => store.clear(), }) function setPlatform(platform: string): void { ;(window as unknown as { codeburn?: { platform?: string } }).codeburn = { platform } } describe('Sidebar', () => { beforeEach(() => { bridge.companionStatus.mockResolvedValue({ supported: false, menuBar: false, sidebar: false, store: false }) }) afterEach(() => { delete (window as unknown as { codeburn?: { platform?: string } }).codeburn store.clear() vi.clearAllMocks() }) it.each([ ['darwin', '⌘'], ['win32', 'Ctrl+'], ] as const)('renders every nav item in its group with %s keycaps', (platform, mod) => { setPlatform(platform) const { container } = render( {}} />) const labels = [...container.querySelectorAll('.ni')].map(item => item.textContent) expect(labels).toEqual(['Overview', 'Sessions', 'Pull requests', 'Spend', 'Models', 'Optimize', 'Compare', 'Compare periods', 'Plans', 'Plugins', 'Settings']) const tip = (label: string) => [...container.querySelectorAll('.ni')].find(item => item.textContent === label)?.getAttribute('title') expect(tip('Sessions')).toBe(`Sessions ${mod}2`) expect(tip('Pull requests')).toBe(`Pull requests ${mod}3`) expect(tip('Compare')).toBe(`Compare ${mod}7`) expect(tip('Plans')).toBe(`Plans ${mod}8`) }) it('calls onNavigate with the section id when a nav item is clicked', () => { const onNavigate = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: /Spend/ })) expect(onNavigate).toHaveBeenCalledWith('spend') }) it('marks the active item with the "on" class', () => { render( {}} />) expect(screen.getByRole('button', { name: /Models/ })).toHaveClass('on') expect(screen.getByRole('button', { name: /Overview/ })).not.toHaveClass('on') }) it('renders the wordmark as animated text with no flame image', () => { const { container } = render( {}} />) const mark = container.querySelector('.app b') expect(mark).toHaveClass('flame-text') expect(mark).toHaveTextContent('CodeBurn') expect(container.querySelector('.app img')).toBeNull() }) it('groups the nav under muted section labels', () => { const { container } = render( {}} />) expect([...container.querySelectorAll('.grp-label')].map(el => el.textContent)).toEqual(['Usage', 'Insight', 'Account']) }) it('keeps the shortcut on the row tooltip rather than a chip in the row', () => { setPlatform('darwin') const { container } = render( {}} />) expect(container.querySelector('.ni .k')).toBeNull() const spend = [...container.querySelectorAll('.ni')].find(item => item.textContent === 'Spend') expect(spend).toHaveAttribute('title', 'Spend ⌘4') expect(spend).toHaveAttribute('data-tip', 'Spend ⌘4') }) it('carries About and the version in the corner, with the links in the modal', async () => { setPlatform('darwin') const { container } = render( {}} />) const about = screen.getByRole('link', { name: /About/ }) expect(about).toHaveTextContent(/^Aboutv\d+\.\d+\.\d+$/) expect(container.querySelector('.foot .social')).toBeNull() fireEvent.click(about) expect(await screen.findByRole('link', { name: /GitHub/ })).toBeInTheDocument() expect(screen.getByRole('link', { name: /LinkedIn/ })).toBeInTheDocument() }) it('collapses to a rail, remembers it, and still navigates by icon', () => { const onNavigate = vi.fn() const { container, unmount } = render() const nav = container.querySelector('.sb') expect(nav).not.toHaveClass('collapsed') fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' })) expect(nav).toHaveClass('collapsed') expect(localStorage.getItem('codeburn.sidebarCollapsed')).toBe('1') // The label never leaves the DOM, so the row keeps its name on the rail. fireEvent.click(screen.getByRole('button', { name: /Spend/ })) expect(onNavigate).toHaveBeenCalledWith('spend') unmount() render( {}} />) expect(document.querySelector('.sb')).toHaveClass('collapsed') expect(screen.getByRole('button', { name: 'Expand sidebar' })).toBeInTheDocument() }) it.each([ ['darwin', { metaKey: true }], ['win32', { ctrlKey: true }], ] as const)('toggles the rail with the %s modifier chord and B', (platform, chord) => { setPlatform(platform) const { container } = render( {}} />) const nav = container.querySelector('.sb') fireEvent.keyDown(window, { key: 'b', ...chord }) expect(nav).toHaveClass('collapsed') fireEvent.keyDown(window, { key: 'b', ...chord }) expect(nav).not.toHaveClass('collapsed') }) // The Menu bar and Capacity Dock switches moved out of the sidebar corner into a card on the // Plugins page, so the sidebar no longer renders any switch. See WindowsCompanionCard.test.tsx. it('renders no companion switch in the corner on Windows', async () => { setPlatform('win32') render( {}} />) expect(screen.getByRole('link', { name: /About/ })).toBeInTheDocument() expect(screen.queryByRole('switch')).toBeNull() }) })