codeburn/app/renderer/components/Sidebar.test.tsx
iamtoruk 2bd44113c7 feat(app): two switches where the social glyphs were
The sidebar's bottom corner held About and a row of five brand glyphs. The
glyphs were the least useful thing on that screen: they are visited once, and
About already listed every one of them under Links. So they are only there now,
and the corner carries the two things a Windows user does want at hand: whether
CodeBurn sits in the notification area, and whether the Capacity Dock rail is on
the screen edge.

Both are on by default and neither renders at all until the main process says
this build has a tray app staged, which is why there is no placeholder row and
no greyed-out switch: on macOS, on Linux, and in a dev build with nothing
staged, the corner is exactly what it always was.

The switch is the shared one from Settings and onboarding, at 30x17 rather than
38x22 because a 186px sidebar has no room for the full size beside a label, and
keyed off the theme tokens rather than the white overlays the bare shape uses on
the dark onboarding sheet.
2026-09-02 04:43:07 -07:00

156 lines
7 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent, waitFor } 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(),
setMenuBarEnabled: vi.fn(),
setSidebarEnabled: vi.fn(),
openExternal: vi.fn(),
}))
vi.mock('../lib/ipc', () => ({ codeburn: bridge, normalizeCliError: (err: unknown) => err }))
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
vi.clearAllMocks()
})
it.each([
['darwin', '⌘'],
['win32', 'Ctrl+'],
] as const)('renders all nav items in the desktop order with %s keycaps', (platform, mod) => {
setPlatform(platform)
render(<Sidebar active="overview" onNavigate={() => {}} />)
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const labels = screen.getAllByRole('button').map(item => item.textContent?.replace(/(⌘|Ctrl\+)[\d,.]/, ''))
expect(labels).toEqual(['Overview', 'Sessions', 'Pull requests', 'Spend', 'Optimize', 'Models', 'Compare', 'Plans', 'Settings', 'Plugins'])
expect(screen.getByRole('button', { name: new RegExp(`Sessions.*${esc(mod)}2`) })).toBeInTheDocument()
expect(screen.getByRole('button', { name: new RegExp(`Pull requests.*${esc(mod)}3`) })).toBeInTheDocument()
expect(screen.getByRole('button', { name: new RegExp(`Compare.*${esc(mod)}7`) })).toBeInTheDocument()
expect(screen.getByRole('button', { name: new RegExp(`Plans.*${esc(mod)}8`) })).toBeInTheDocument()
})
it('calls onNavigate with the section id when a nav item is clicked', () => {
const onNavigate = vi.fn()
render(<Sidebar active="overview" onNavigate={onNavigate} />)
fireEvent.click(screen.getByRole('button', { name: /Spend/ }))
expect(onNavigate).toHaveBeenCalledWith('spend')
})
it('marks the active item with the "on" class', () => {
render(<Sidebar active="models" onNavigate={() => {}} />)
expect(screen.getByRole('button', { name: /Models/ })).toHaveClass('on')
expect(screen.getByRole('button', { name: /Overview/ })).not.toHaveClass('on')
})
it('renders the brand flame mark, static under the closed motion gate', () => {
const { container } = render(<Sidebar active="overview" onNavigate={() => {}} />)
const flame = container.querySelector('.app .flamemark')
expect(flame?.tagName.toLowerCase()).toBe('img')
// motionEnabled() is off under vitest, so the idle flicker never attaches.
expect(container.querySelector('.fm-flicker')).toBeNull()
})
it('keeps About in the corner and the social glyphs out of it', async () => {
const { container } = render(<Sidebar active="overview" onNavigate={() => {}} />)
expect(screen.getByRole('link', { name: 'About' })).toBeInTheDocument()
expect(container.querySelector('.foot .social')).toBeNull()
// They moved into the About modal, which lists them under Links.
fireEvent.click(screen.getByRole('link', { name: 'About' }))
expect(await screen.findByRole('link', { name: /GitHub/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /LinkedIn/ })).toBeInTheDocument()
})
})
describe('Sidebar companion switches', () => {
const SUPPORTED = { supported: true, menuBar: true, sidebar: true, store: false }
afterEach(() => { vi.clearAllMocks() })
async function renderSwitches(status = SUPPORTED) {
bridge.companionStatus.mockResolvedValue(status)
render(<Sidebar active="overview" onNavigate={() => {}} />)
return screen.findByRole('switch', { name: 'Menu bar' })
}
it('shows nothing where the main process reports no bundled tray app', async () => {
bridge.companionStatus.mockResolvedValue({ supported: false, menuBar: false, sidebar: false, store: false })
render(<Sidebar active="overview" onNavigate={() => {}} />)
await waitFor(() => expect(bridge.companionStatus).toHaveBeenCalled())
expect(screen.queryByRole('switch')).toBeNull()
})
it('survives a preload that has never heard of them', async () => {
bridge.companionStatus.mockRejectedValue(new Error('no such channel'))
render(<Sidebar active="overview" onNavigate={() => {}} />)
await waitFor(() => expect(bridge.companionStatus).toHaveBeenCalled())
expect(screen.queryByRole('switch')).toBeNull()
})
it('renders both switches on, in the sidebar corner', async () => {
const menuBar = await renderSwitches()
expect(menuBar).toHaveAttribute('aria-checked', 'true')
expect(screen.getByRole('switch', { name: 'Sidebar' })).toHaveAttribute('aria-checked', 'true')
})
it('turning Menu bar off sends false and renders the status that came back', async () => {
const menuBar = await renderSwitches()
bridge.setMenuBarEnabled.mockResolvedValue({ ...SUPPORTED, menuBar: false })
fireEvent.click(menuBar)
expect(bridge.setMenuBarEnabled).toHaveBeenCalledWith(false)
await waitFor(() => expect(menuBar).toHaveAttribute('aria-checked', 'false'))
expect(screen.getByRole('switch', { name: 'Sidebar' })).toHaveAttribute('aria-checked', 'true')
})
it('turning Sidebar off leaves Menu bar alone', async () => {
await renderSwitches()
bridge.setSidebarEnabled.mockResolvedValue({ ...SUPPORTED, sidebar: false })
fireEvent.click(screen.getByRole('switch', { name: 'Sidebar' }))
expect(bridge.setSidebarEnabled).toHaveBeenCalledWith(false)
await waitFor(() => expect(screen.getByRole('switch', { name: 'Sidebar' })).toHaveAttribute('aria-checked', 'false'))
expect(screen.getByRole('switch', { name: 'Menu bar' })).toHaveAttribute('aria-checked', 'true')
})
/// An install the person cancelled at the UAC prompt comes back unchanged, and the switch
/// has to show that rather than the state it optimistically painted.
it('stays where it was when the main process reports no change', async () => {
const menuBar = await renderSwitches({ ...SUPPORTED, menuBar: false })
bridge.setMenuBarEnabled.mockResolvedValue({ ...SUPPORTED, menuBar: false })
fireEvent.click(menuBar)
await waitFor(() => expect(bridge.setMenuBarEnabled).toHaveBeenCalledWith(true))
expect(menuBar).toHaveAttribute('aria-checked', 'false')
})
it('refuses a second click while one is still in flight', async () => {
const menuBar = await renderSwitches()
bridge.setMenuBarEnabled.mockReturnValue(new Promise(() => {}))
fireEvent.click(menuBar)
fireEvent.click(screen.getByRole('switch', { name: 'Sidebar' }))
expect(bridge.setMenuBarEnabled).toHaveBeenCalledTimes(1)
expect(bridge.setSidebarEnabled).not.toHaveBeenCalled()
})
})