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') }) })