Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269// 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( <LayoutProvider> <SidebarViewContainer app={app} side="left" /> </LayoutProvider>, )
// 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( <LayoutProvider> <SidebarViewContainer app={app} side="left" /> </LayoutProvider>, )
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') })})