// src/layout/__tests__/SidebarViewContainer.sort.test.tsx // // Integration smoke for the chrome-row live update contract using a REAL // FileTreeView (not a hand-rolled stub). This is Test 16 in the test // plan and is the artifact-based proof of the full production pipeline: // // menu click → SortMenuButton.onChange → FileTreeView.setSortMode // → view-sort-store + sort-mode-persistence + requestHeaderActionsUpdate // → Workspace._requestHeaderActionsUpdate // → reamsEvents.trigger('view-header-actions-change', leaf) // → SidebarViewContainer's useState counter bumps // → headerActions useMemo re-runs // → SortMenuButton receives new currentMode prop // → aria-label flips // → vault state stamped. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { render, cleanup, act, fireEvent, within, } from '@testing-library/react' import type { AutomergeUrl } from '@automerge/automerge-repo' import { App } from '../../plugin/app' import { Vault } from '../../plugin/vault' import { FileIndex } from '../../services/file-index' import { Workspace } from '../../plugin/workspace' import { MetadataCache } from '../../services/metadata' import { ReamsEvents } from '../../plugin/reams/events' import { createTFile } from '../../plugin/files' import { SidebarViewContainer } from '../SidebarViewContainer' import { LayoutProvider } from '../LayoutContext' import { FileTreeView, FILE_TREE_VIEW_TYPE, } from '../../plugins/file-tree/file-tree-view' import { QuickAccessView, QUICK_ACCESS_VIEW_TYPE, } from '../../plugins/quick-access/quick-access-view' import { SORT_MODES, getSortModeMetadata } from '../../plugin/sidebar/sort-mode' import { openTestVaultPreferences } from '../../services/__tests__/vault-state-test-helpers' import { readVaultPreference, type VaultPreferences } from '../../services/vault-preferences' // FileTree pulls metadata from SearchContext via useSearch(). vi.mock('../../services/SearchContext', () => ({ useSearch: () => ({ metadataCache: { subscribe: () => () => {}, getVersion: () => 0, getDisplayTitle: () => null, getIndexedDocuments: () => [], on: () => ({ e: {}, n: 'changed', fn: () => {} }), offref: () => {}, }, }), })) interface Harness { app: App } interface FileSpec { path: string ctime: number mtime: number } function makeHarness(files: FileSpec[] = []): Harness { const mockDocumentManager = { createFile: vi.fn(), createFolder: vi.fn(), readText: vi.fn(async (url: AutomergeUrl) => ({ status: 'available' as const, documentId: url, heads: ['h0'], content: '' })), updateContent: vi.fn(), listFolder: vi.fn().mockResolvedValue([]), createFileInFolder: vi.fn(), getFile: vi.fn().mockResolvedValue(null), } as any const fileIndex = new FileIndex(mockDocumentManager, 'automerge:root' as AutomergeUrl) const vault = new Vault('Test', fileIndex, mockDocumentManager) const workspace = new Workspace(document.createElement('div')) const metadataCache = new MetadataCache() metadataCache.setVault(vault) const app = new App(vault, workspace) app.setMetadataCache(metadataCache) app.setDocumentManager(mockDocumentManager) const root = vault.getRoot() for (const spec of files) { const file = createTFile(spec.path, root, { ctime: spec.ctime, mtime: spec.mtime, size: 0, }) file.vault = vault root.children.push(file) fileIndex.registerFile( `automerge:${spec.path}` as AutomergeUrl, 'automerge:root' as AutomergeUrl, file, ) } // SidebarViewContainer subscribes to app.reamsEvents — App ctor already // instantiates it, but we double-check to keep the harness explicit. if (!(app as any).reamsEvents) { ;(app as any).reamsEvents = new ReamsEvents() } return { app } } describe('SidebarViewContainer integration: live sort-icon update', () => { let harness: Harness | null = null let preferences: VaultPreferences beforeEach(async () => { localStorage.clear() preferences = await openTestVaultPreferences() harness = null }) afterEach(() => { cleanup() harness?.app.destroy() preferences.close() harness = null document.querySelectorAll('.menu').forEach((m) => m.remove()) }) it('FileTreeView: a real menu click updates the icon and visible row order', async () => { harness = makeHarness([ { path: 'apple.md', ctime: 300, mtime: 200 }, { path: 'banana.md', ctime: 100, mtime: 300 }, { path: 'cherry.md', ctime: 200, mtime: 100 }, ]) const { app } = harness const workspace = app.workspace workspace.registerViewType( FILE_TREE_VIEW_TYPE, (leaf) => new FileTreeView(leaf, app), ) const leaf = workspace.getLeftLeaf(true) await leaf.openView(FILE_TREE_VIEW_TYPE) const { container } = render( , ) // Scope all button queries to the chrome-row header — the FileTree // body renders its own buttons and we don't want to accidentally // catch one of those. const header = container.querySelector('.sidebar-view-header') as HTMLElement expect(header).not.toBeNull() // Initial: default mode is `name-asc`. The sort button's aria-label // includes that mode's label. const initialBtn = within(header).getByRole('button', { name: /Sort:/ }) expect(initialBtn.getAttribute('aria-label')).toContain( getSortModeMetadata('name-asc').label, ) expect(within(container).getAllByRole('treeitem').map((row) => row.textContent?.trim())).toEqual([ 'apple.md', 'banana.md', 'cherry.md', ]) // Open the menu, click the modified-desc item. act(() => { fireEvent.click(initialBtn) }) const items = document.querySelectorAll('.menu .menu-item') expect(items.length).toBe(SORT_MODES.length) // Index lookup against SORT_MODES so the test is robust to future // reordering of the canonical mode list. const targetIdx = SORT_MODES.indexOf('modified-desc') act(() => { ;(items[targetIdx] as HTMLElement).click() }) // Chrome-row icon updates without a tab switch — the button's // aria-label now reflects the new mode. Re-query (the React tree // re-rendered, the button instance may have changed). const updatedBtn = within(header).getByRole('button', { name: /Sort:/ }) expect(updatedBtn.getAttribute('aria-label')).toContain( getSortModeMetadata('modified-desc').label, ) expect(within(container).getAllByRole('treeitem').map((row) => row.textContent?.trim())).toEqual([ 'banana.md', 'apple.md', 'cherry.md', ]) // Persistence reflects the new mode at the file-tree view key. const raw = readVaultPreference('reams:sidebar-sort-modes') expect(raw).not.toBeNull() const parsed = JSON.parse(raw!) expect(parsed.version).toBe(1) expect(parsed.modes[FILE_TREE_VIEW_TYPE]).toBe('modified-desc') }) it('QuickAccessView: a real menu click updates the icon and visible row order', async () => { harness = makeHarness([ { path: 'apple.md', ctime: 300, mtime: 200 }, { path: 'banana.md', ctime: 100, mtime: 300 }, { path: 'cherry.md', ctime: 200, mtime: 100 }, ]) const { app } = harness const workspace = app.workspace workspace.registerViewType( QUICK_ACCESS_VIEW_TYPE, (leaf) => new QuickAccessView(leaf, app), ) const leaf = workspace.getLeftLeaf(true) await leaf.openView(QUICK_ACCESS_VIEW_TYPE) const { container } = render( , ) const header = container.querySelector('.sidebar-view-header') as HTMLElement expect(header).not.toBeNull() const initialBtn = within(header).getByRole('button', { name: /Sort:/ }) expect(initialBtn.getAttribute('aria-label')).toContain( getSortModeMetadata('name-asc').label, ) expect( within(container) .getAllByTestId('quick-access-row') .map((row) => row.getAttribute('data-path')), ).toEqual(['apple.md', 'banana.md', 'cherry.md']) act(() => { fireEvent.click(initialBtn) }) const items = document.querySelectorAll('.menu .menu-item') expect(items.length).toBe(SORT_MODES.length) const targetIdx = SORT_MODES.indexOf('created-asc') act(() => { ;(items[targetIdx] as HTMLElement).click() }) const updatedBtn = within(header).getByRole('button', { name: /Sort:/ }) expect(updatedBtn.getAttribute('aria-label')).toContain( getSortModeMetadata('created-asc').label, ) expect( within(container) .getAllByTestId('quick-access-row') .map((row) => row.getAttribute('data-path')), ).toEqual(['banana.md', 'cherry.md', 'apple.md']) const raw = readVaultPreference('reams:sidebar-sort-modes') expect(raw).not.toBeNull() const parsed = JSON.parse(raw!) expect(parsed.modes[QUICK_ACCESS_VIEW_TYPE]).toBe('created-asc') }) })