// 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()
})
})
})