import { render, screen, act } from '@testing-library/react'
import { fireEvent } from '@testing-library/react'
import { renderHook } from '@testing-library/react'
import { CommandPaletteProvider, useCommandPalette } from '../command-palette-context'
// Wrapper that exposes the context value through data attributes so we can
// assert on it without tying tests to component internals.
function ContextProbe() {
const { isOpen, open, close, toggle } = useCommandPalette()
return (
)
}
function renderWithProvider() {
return render(
)
}
function probe() {
return screen.getByTestId('probe')
}
describe('CommandPaletteProvider / useCommandPalette', () => {
describe('initial state', () => {
it('starts with isOpen = false', () => {
renderWithProvider()
expect(probe().dataset.open).toBe('false')
})
})
describe('open()', () => {
it('sets isOpen to true', () => {
renderWithProvider()
fireEvent.click(screen.getByTestId('btn-open'))
expect(probe().dataset.open).toBe('true')
})
it('is idempotent — calling open twice leaves isOpen true', () => {
renderWithProvider()
fireEvent.click(screen.getByTestId('btn-open'))
fireEvent.click(screen.getByTestId('btn-open'))
expect(probe().dataset.open).toBe('true')
})
})
describe('close()', () => {
it('sets isOpen to false', () => {
renderWithProvider()
fireEvent.click(screen.getByTestId('btn-open'))
fireEvent.click(screen.getByTestId('btn-close'))
expect(probe().dataset.open).toBe('false')
})
it('is idempotent — calling close when already closed keeps isOpen false', () => {
renderWithProvider()
fireEvent.click(screen.getByTestId('btn-close'))
expect(probe().dataset.open).toBe('false')
})
})
describe('toggle()', () => {
it('opens when currently closed', () => {
renderWithProvider()
fireEvent.click(screen.getByTestId('btn-toggle'))
expect(probe().dataset.open).toBe('true')
})
it('closes when currently open', () => {
renderWithProvider()
fireEvent.click(screen.getByTestId('btn-open'))
fireEvent.click(screen.getByTestId('btn-toggle'))
expect(probe().dataset.open).toBe('false')
})
it('toggles back and forth correctly', () => {
renderWithProvider()
const btn = screen.getByTestId('btn-toggle')
fireEvent.click(btn)
expect(probe().dataset.open).toBe('true')
fireEvent.click(btn)
expect(probe().dataset.open).toBe('false')
fireEvent.click(btn)
expect(probe().dataset.open).toBe('true')
})
})
describe('Cmd+K / Ctrl+K keyboard shortcut', () => {
it('toggles isOpen on Cmd+K (metaKey)', () => {
renderWithProvider()
act(() => {
fireEvent.keyDown(document, { key: 'k', metaKey: true })
})
expect(probe().dataset.open).toBe('true')
})
it('toggles isOpen on Ctrl+K (ctrlKey)', () => {
renderWithProvider()
act(() => {
fireEvent.keyDown(document, { key: 'k', ctrlKey: true })
})
expect(probe().dataset.open).toBe('true')
})
it('toggles closed on second Cmd+K', () => {
renderWithProvider()
act(() => { fireEvent.keyDown(document, { key: 'k', metaKey: true }) })
act(() => { fireEvent.keyDown(document, { key: 'k', metaKey: true }) })
expect(probe().dataset.open).toBe('false')
})
it('does not toggle on plain k keypress', () => {
renderWithProvider()
act(() => { fireEvent.keyDown(document, { key: 'k' }) })
expect(probe().dataset.open).toBe('false')
})
it('removes the global keydown listener on unmount (no state update after unmount)', () => {
const { unmount } = renderWithProvider()
unmount()
// Should not throw when dispatching after unmount
expect(() => {
act(() => { fireEvent.keyDown(document, { key: 'k', metaKey: true }) })
}).not.toThrow()
})
})
describe('Escape key', () => {
it('closes the palette when open', () => {
renderWithProvider()
// Open it first
fireEvent.click(screen.getByTestId('btn-open'))
expect(probe().dataset.open).toBe('true')
act(() => { fireEvent.keyDown(document, { key: 'Escape' }) })
expect(probe().dataset.open).toBe('false')
})
it('does nothing when palette is already closed', () => {
renderWithProvider()
act(() => { fireEvent.keyDown(document, { key: 'Escape' }) })
expect(probe().dataset.open).toBe('false')
})
})
describe('useCommandPalette — usage outside provider', () => {
it('throws when used outside CommandPaletteProvider', () => {
// Suppress the expected React error boundary console output
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
expect(() => {
renderHook(() => useCommandPalette())
}).toThrow('useCommandPalette must be used within a CommandPaletteProvider')
spy.mockRestore()
})
})
})