import { fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AppStateProvider, useAppState } from '../src/app'
function Probe() {
const { theme, viewMode, sidebarCollapsed, searchOpen, toggleTheme, setViewMode, toggleSidebar } = useAppState()
return (
{`${theme}|${viewMode}|${sidebarCollapsed}|${searchOpen}`}
)
}
function renderApp() {
return render(
,
)
}
const state = () => screen.getByTestId('state').textContent
describe('AppStateProvider', () => {
beforeEach(() => {
localStorage.clear()
document.documentElement.classList.remove('dark', 'light')
document.documentElement.removeAttribute('data-theme')
document.documentElement.removeAttribute('data-view-mode')
})
afterEach(() => {
vi.restoreAllMocks()
})
it('throws when useAppState is called outside the provider', () => {
// React logs the render error too — keep the output readable.
vi.spyOn(console, 'error').mockImplementation(() => {})
expect(() => render()).toThrow(/AppStateProvider/)
})
it('starts from the defaults and mirrors them onto ', () => {
renderApp()
expect(state()).toBe('dark|default|false|false')
expect(document.documentElement.dataset.theme).toBe('dark')
expect(document.documentElement.dataset.viewMode).toBe('default')
})
it('persists settings on user action only, so prefers-color-scheme keeps working', () => {
renderApp()
// Nothing was stored by the initial mount — an effect-driven write would
// freeze the OS-derived theme for users who never toggled anything.
expect(localStorage.getItem('ns-theme')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'theme' }))
fireEvent.click(screen.getByRole('button', { name: 'compact' }))
fireEvent.click(screen.getByRole('button', { name: 'sidebar' }))
expect(localStorage.getItem('ns-theme')).toBe('light')
expect(localStorage.getItem('ns-view-mode')).toBe('compact')
expect(localStorage.getItem('ns-sidebar-collapsed')).toBe('true')
expect(document.documentElement.dataset.theme).toBe('light')
expect(document.documentElement.dataset.viewMode).toBe('compact')
expect(state()).toBe('light|compact|true|false')
})
it('rehydrates stored settings on the next mount', () => {
localStorage.setItem('ns-theme', 'light')
localStorage.setItem('ns-view-mode', 'compact')
localStorage.setItem('ns-sidebar-collapsed', 'true')
// Theme boots from the attribute index.html already painted (it resolved
// storage / prefers-color-scheme), so state can never contradict the DOM.
document.documentElement.setAttribute('data-theme', 'light')
renderApp()
expect(state()).toBe('light|compact|true|false')
})
it('ignores stored values outside the allowed set', () => {
localStorage.setItem('ns-view-mode', 'space')
localStorage.setItem('ns-sidebar-collapsed', 'yes')
renderApp()
expect(state()).toBe('dark|default|false|false')
})
it('toggles the search palette with ⌘K / Ctrl+K from anywhere', () => {
function SearchProbe() {
const { searchOpen } = useAppState()
return {String(searchOpen)}
}
render(
,
)
fireEvent.keyDown(window, { key: 'k', metaKey: true })
expect(screen.getByTestId('search')).toHaveTextContent('true')
fireEvent.keyDown(window, { key: 'k', ctrlKey: true })
expect(screen.getByTestId('search')).toHaveTextContent('false')
// A plain "k" must not hijack typing.
fireEvent.keyDown(window, { key: 'k' })
expect(screen.getByTestId('search')).toHaveTextContent('false')
})
})