mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-23 15:34:19 +00:00
Closes #918. The renderer's keydown handler required event.metaKey and explicitly rejected event.ctrlKey, so every shortcut was dead outside macOS: on Windows and Linux metaKey is the Super key, which the OS shell takes. Navigation (1-8), Settings (,) and Refresh (R) all did nothing. The sidebar and footer hints also hardcoded the Cmd glyph, so a Windows user was shown chords that could not fire. Add app/renderer/lib/platform.ts as the single source of truth for platform-aware shortcuts, reading the platform the preload already exposes (window.codeburn.platform) with a user-agent fallback for the non-Electron cases. isModifierChord accepts Cmd-without-Ctrl on darwin and Ctrl-without-Cmd elsewhere; altKey stays rejected on both, because AltGr on European Windows layouts arrives as Ctrl+Alt and must not hijack a typed character. Every visible shortcut label now resolves through shortcutLabel() at render time, so the sidebar shows Ctrl+1 where macOS shows the Cmd glyph. The mac chord condition is unchanged: the old guard admitted metaKey && !altKey && !ctrlKey && !shiftKey, and the new one admits exactly the same set on darwin. The Electron application menu is deliberately left alone. It ships no reload/forceReload role and no CmdOrCtrl+R accelerator, which is what leaves Ctrl+R free for the renderer to handle on Windows. Also corrects the Settings navigation hint, which read 1-7 while the sidebar has eight numbered destinations. Tests cover both platforms for labels and dispatch, including the negatives: Meta on win32, Ctrl on darwin, and the Ctrl+Alt AltGr shape.
51 lines
2.4 KiB
TypeScript
51 lines
2.4 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, describe, it, expect, vi } from 'vitest'
|
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
|
|
import { Sidebar } from './Sidebar'
|
|
|
|
function setPlatform(platform: string): void {
|
|
;(window as unknown as { codeburn?: { platform?: string } }).codeburn = { platform }
|
|
}
|
|
|
|
describe('Sidebar', () => {
|
|
afterEach(() => {
|
|
delete (window as unknown as { codeburn?: { platform?: string } }).codeburn
|
|
})
|
|
|
|
it.each([
|
|
['darwin', '⌘'],
|
|
['win32', 'Ctrl+'],
|
|
] as const)('renders all nine 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'])
|
|
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()
|
|
})
|
|
})
|