// src/layout/__tests__/LayoutContext.test.tsx import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { render, screen, act, cleanup } from '@testing-library/react' import { LayoutProvider, useLayout } from '../LayoutContext' import { LAYOUT_STORAGE_KEY } from '../layout-persistence' import type { ReactNode } from 'react' import { VaultPreferences } from '../../services/vault-preferences' import { createTestVaultStateMap } from '../../services/__tests__/vault-state-test-helpers' // Test component that consumes the context function TestConsumer() { const layout = useLayout() return (
{layout.state.sidebarWidth} {layout.state.sidebarMode} {layout.state.editorMode}
) } function renderWithProvider(children: ReactNode) { return render({children}) } describe('LayoutContext', () => { beforeEach(() => { localStorage.clear() }) afterEach(() => { cleanup() }) describe('LayoutProvider', () => { it('provides default state', () => { renderWithProvider() expect(screen.getByTestId('sidebar-width').textContent).toBe('240') expect(screen.getByTestId('sidebar-mode').textContent).toBe('full') expect(screen.getByTestId('editor-mode').textContent).toBe('live-preview') }) it('loads state from localStorage on mount', () => { localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify({ version: 5, sidebarWidth: 300, sidebarMode: 'hidden', rightPanelWidth: 280, rightPanelCollapsed: true, editorMode: 'source', })) renderWithProvider() expect(screen.getByTestId('sidebar-width').textContent).toBe('300') expect(screen.getByTestId('sidebar-mode').textContent).toBe('hidden') expect(screen.getByTestId('editor-mode').textContent).toBe('source') }) it('uses vault mode over conflicting device state and follows remote preference changes', async () => { const map = createTestVaultStateMap() await map.set('reams:editor-mode', 'live-preview') const preferences = new VaultPreferences(map) await preferences.open() localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify({ version: 5, editorMode: 'source' })) function ModeControls() { const layout = useLayout() return <> } const view = render() try { expect(screen.getByTestId('editor-mode')).toHaveTextContent('live-preview') expect(JSON.parse(localStorage.getItem(LAYOUT_STORAGE_KEY)!)).not.toHaveProperty('editorMode') act(() => { screen.getByText('Source preference').click() }) expect(preferences.get('reams:editor-mode')).toBe('source') expect(map.snapshot()['reams:editor-mode']).toBe('source') await act(async () => { await map.set('reams:editor-mode', 'live-preview') }) expect(screen.getByTestId('editor-mode')).toHaveTextContent('live-preview') } finally { view.unmount(); preferences.close() } }) it('defaults a fresh vault to live-preview without importing the device mode', async () => { const map = createTestVaultStateMap() const preferences = new VaultPreferences(map) await preferences.open() localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify({ version: 5, editorMode: 'source' })) const view = render() expect(map.snapshot()['reams:editor-mode']).toBeUndefined() expect(JSON.parse(localStorage.getItem(LAYOUT_STORAGE_KEY)!)).not.toHaveProperty('editorMode') view.unmount() const reopened = render() try { expect(screen.getByTestId('editor-mode')).toHaveTextContent('live-preview') } finally { reopened.unmount(); preferences.close() } }) it('does not migrate the previous vault mode when switching to a fresh vault', async () => { const mapA = createTestVaultStateMap(), mapB = createTestVaultStateMap() await mapA.set('reams:editor-mode', 'source') const a = new VaultPreferences(mapA), b = new VaultPreferences(mapB) await a.open(); await b.open() const view = render() try { expect(screen.getByTestId('editor-mode')).toHaveTextContent('source') view.rerender() expect(screen.getByTestId('editor-mode')).toHaveTextContent('live-preview') expect(b.get('reams:editor-mode')).toBeNull() expect(a.get('reams:editor-mode')).toBe('source') } finally { view.unmount(); a.close(); b.close() } }) }) describe('toggleSidebar', () => { it('cycles sidebar mode: full -> hidden -> ribbon -> full', () => { renderWithProvider() // Default is 'full' expect(screen.getByTestId('sidebar-mode').textContent).toBe('full') // full -> hidden act(() => { screen.getByText('Toggle Sidebar').click() }) expect(screen.getByTestId('sidebar-mode').textContent).toBe('hidden') // hidden -> ribbon act(() => { screen.getByText('Toggle Sidebar').click() }) expect(screen.getByTestId('sidebar-mode').textContent).toBe('ribbon') // ribbon -> full act(() => { screen.getByText('Toggle Sidebar').click() }) expect(screen.getByTestId('sidebar-mode').textContent).toBe('full') }) it('persists to localStorage', () => { renderWithProvider() act(() => { screen.getByText('Toggle Sidebar').click() }) const stored = JSON.parse(localStorage.getItem(LAYOUT_STORAGE_KEY)!) expect(stored.sidebarMode).toBe('hidden') }) }) describe('setSidebarMode', () => { it('sets sidebar mode directly', () => { function TestWithSetMode() { const layout = useLayout() return (
{layout.state.sidebarMode}
) } renderWithProvider() expect(screen.getByTestId('sidebar-mode').textContent).toBe('full') act(() => { screen.getByText('Set Ribbon').click() }) expect(screen.getByTestId('sidebar-mode').textContent).toBe('ribbon') act(() => { screen.getByText('Set Hidden').click() }) expect(screen.getByTestId('sidebar-mode').textContent).toBe('hidden') }) }) describe('setRightPanelCollapsed', () => { it('sets right panel collapsed directly', () => { function TestWithSetCollapsed() { const layout = useLayout() return (
{String(layout.state.rightPanelCollapsed)}
) } renderWithProvider() // Right panel starts collapsed by default expect(screen.getByTestId('right-collapsed').textContent).toBe('true') act(() => { screen.getByText('Expand').click() }) expect(screen.getByTestId('right-collapsed').textContent).toBe('false') act(() => { screen.getByText('Collapse').click() }) expect(screen.getByTestId('right-collapsed').textContent).toBe('true') }) }) describe('setEditorMode', () => { it('sets editor mode directly', () => { function TestWithSetEditorMode() { const layout = useLayout() return (
{layout.state.editorMode}
) } renderWithProvider() // Default is 'live-preview' expect(screen.getByTestId('editor-mode').textContent).toBe('live-preview') act(() => { screen.getByText('Set Source').click() }) expect(screen.getByTestId('editor-mode').textContent).toBe('source') act(() => { screen.getByText('Set Live Preview').click() }) expect(screen.getByTestId('editor-mode').textContent).toBe('live-preview') }) }) describe('useLayout outside provider', () => { it('throws an error', () => { // Suppress console.error for this test const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) expect(() => { render() }).toThrow('useLayout must be used within a LayoutProvider') consoleSpy.mockRestore() }) }) })