diff --git a/src/App.test.tsx b/src/App.test.tsx index 18506ce2..3eddbb63 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -525,7 +525,7 @@ describe('App', () => { // Entries appear in both Sidebar and NoteList expect(screen.getAllByText('Test Project').length).toBeGreaterThan(0) expect(screen.getAllByText('Software Development').length).toBeGreaterThan(0) - }) + }, { timeout: SLOW_APP_READY_TIMEOUT_MS }) }) it('keeps the app shell usable while the vault note scan is pending', async () => { @@ -1301,7 +1301,7 @@ describe('App', () => { render() - expect(await screen.findByTestId('status-missing-git')).toBeInTheDocument() + expect(await screen.findByTestId('status-missing-git', {}, { timeout: SLOW_APP_READY_TIMEOUT_MS })).toBeInTheDocument() fireEvent.click(screen.getByTestId('status-missing-git')) expect(await screen.findByText('Enable Git for this vault?')).toBeInTheDocument() diff --git a/src/hooks/useVaultLoader.test.ts b/src/hooks/useVaultLoader.test.ts index 0050c5c4..fe62fc60 100644 --- a/src/hooks/useVaultLoader.test.ts +++ b/src/hooks/useVaultLoader.test.ts @@ -143,10 +143,14 @@ async function enableTauriMode() { ) } -function buildMountedWorkspaceLoadMock() { +function buildMountedWorkspaceLoadMock(options: { + pendingEntriesByPath?: Record> +} = {}) { return ((cmd: string, args?: Record) => { if (isVaultLoadCommand(cmd)) { - const path = args?.path + const path = typeof args?.path === 'string' ? args.path : '' + const pendingEntries = options.pendingEntriesByPath?.[path] + if (pendingEntries) return pendingEntries return Promise.resolve([{ ...mockEntries[0], path: `${path}/note/hello.md`, @@ -229,7 +233,7 @@ describe('useVaultLoader', () => { }) }) - it('does not re-add the base vault as a folder root after it is unmounted', async () => { + it('keeps the active vault folder root visible when it is absent from mounted folder vaults', async () => { const entryVaults = [ { label: 'Brian', path: '/brian', alias: 'brian', available: true, mounted: false }, { label: 'Laputa', path: '/laputa', alias: 'laputa', available: true, mounted: true }, @@ -261,6 +265,12 @@ describe('useVaultLoader', () => { rootPath: '/laputa', children: [{ name: 'laputa-projects', path: 'projects', rootPath: '/laputa', children: [] }], }, + { + name: 'brian', + path: '', + rootPath: '/brian', + children: [{ name: 'brian-projects', path: 'projects', rootPath: '/brian', children: [] }], + }, ]) }) }) @@ -372,25 +382,15 @@ describe('useVaultLoader', () => { }) it('adds each mounted workspace as soon as that workspace finishes loading', async () => { - let resolveLaputa: ((entries: VaultEntry[]) => void) | null = null + const laputaLoad = createDeferred() const brian = { label: 'Brian', path: '/brian', alias: 'brian', available: true, mounted: true } const laputa = { label: 'Laputa', path: '/laputa', alias: 'laputa', available: true, mounted: true } const team = { label: 'Team', path: '/team', alias: 'team', available: true, mounted: true } const vaults = [brian, laputa, team] - backendInvokeFn.mockImplementation(((cmd: string, args?: Record) => { - const path = args?.path - if (isVaultLoadCommand(cmd)) { - if (path === '/laputa') { - return new Promise((resolve) => { - resolveLaputa = resolve - }) - } - return Promise.resolve([{ ...mockEntries[0], path: `${path}/note/hello.md` }]) - } - if (cmd === 'list_vault_folders' || cmd === 'list_views' || cmd === 'get_modified_files') return Promise.resolve([]) - return Promise.resolve(null) - }) as typeof defaultMockInvoke) + backendInvokeFn.mockImplementation(buildMountedWorkspaceLoadMock({ + pendingEntriesByPath: { '/laputa': laputaLoad.promise }, + })) const { result } = renderHook(() => useVaultLoader('/brian', vaults, '/brian', vaults)) @@ -399,7 +399,7 @@ describe('useVaultLoader', () => { }) await act(async () => { - resolveLaputa?.([{ ...mockEntries[0], path: '/laputa/note/hello.md' }]) + laputaLoad.resolve([{ ...mockEntries[0], path: '/laputa/note/hello.md' }]) }) await waitFor(() => { @@ -662,15 +662,17 @@ describe('useVaultLoader', () => { const brian = { label: 'Brian', path: '/brian', alias: 'brian', available: true, mounted: true } const laputa = { label: 'Laputa', path: '/laputa', alias: 'laputa', available: true, mounted: true } const vaults = [laputa, brian] + const laputaStartupResponses: Partial> = { + reload_vault: [ + { ...mockEntries[0], path: '/laputa/note/alpha.md', filename: 'alpha.md', title: 'Alpha' }, + ], + list_vault: [], + } backendInvokeFn.mockImplementation(((cmd: string, args?: Record) => { - if (cmd === 'reload_vault' && args?.path === '/laputa') { - return Promise.resolve([ - { ...mockEntries[0], path: '/laputa/note/alpha.md', filename: 'alpha.md', title: 'Alpha' }, - ]) - } - if (cmd === 'list_vault' && args?.path === '/laputa') return Promise.resolve([]) - if (cmd === 'list_vault_folders' || cmd === 'list_views' || cmd === 'get_modified_files') return Promise.resolve([]) + const response = args?.path === '/laputa' ? laputaStartupResponses[cmd] : undefined + if (response) return Promise.resolve(response) + if (EMPTY_ARRAY_COMMANDS.has(cmd)) return Promise.resolve([]) return Promise.resolve(null) }) as typeof defaultMockInvoke) diff --git a/src/hooks/vaultLoaderCommands.ts b/src/hooks/vaultLoaderCommands.ts index 1f5e10b5..6587a779 100644 --- a/src/hooks/vaultLoaderCommands.ts +++ b/src/hooks/vaultLoaderCommands.ts @@ -173,7 +173,7 @@ export function loadVaultFolders({ vaultPath }: VaultPathOptions): Promise { - const mountedVaults = uniqueMountedVaults({ ...options, includeFallbackVault: false }) + const mountedVaults = uniqueMountedVaults(options) if (mountedVaults.length === 0) return [] if (mountedVaults.length === 1) { const [vault] = mountedVaults @@ -213,7 +213,7 @@ export function loadVaultViews({ vaultPath }: VaultPathOptions): Promise { - const mountedVaults = uniqueMountedVaults({ ...options, includeFallbackVault: false }) + const mountedVaults = uniqueMountedVaults(options) if (mountedVaults.length === 0) return [] if (mountedVaults.length === 1 && mountedVaults[0].path === options.vaultPath) { return loadVaultViews({ vaultPath: options.vaultPath }) diff --git a/src/hooks/vaultLoaderCommands.views.test.ts b/src/hooks/vaultLoaderCommands.views.test.ts index 5a0e6ae6..07a5baa8 100644 --- a/src/hooks/vaultLoaderCommands.views.test.ts +++ b/src/hooks/vaultLoaderCommands.views.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { loadMountedVaultViews } from './vaultLoaderCommands' +import { loadMountedVaultFolders, loadMountedVaultViews } from './vaultLoaderCommands' import type { ViewDefinition } from '../types' const mockInvoke = vi.fn() @@ -86,4 +86,93 @@ describe('loadMountedVaultViews', () => { expect(views.map((view) => view.definition.name)).toEqual(['Default View', 'Mounted View']) }) + + it('loads saved views from the active workspace when folder vaults omit it', async () => { + mockInvoke.mockImplementation((command: string, args?: Record) => { + if (command !== 'list_views') return Promise.resolve([]) + + if (args?.vaultPath === '/active') { + return Promise.resolve([{ filename: 'active.yml', definition: viewDefinition('Active View') }]) + } + if (args?.vaultPath === '/mounted') { + return Promise.resolve([{ filename: 'mounted.yml', definition: viewDefinition('Mounted View') }]) + } + throw new Error(`Unexpected vaultPath ${String(args?.vaultPath)}`) + }) + + const views = await loadMountedVaultViews({ + defaultWorkspacePath: '/mounted', + vaultPath: '/active', + vaults: [ + { label: 'Mounted', path: '/mounted', alias: 'mounted', mounted: true, available: true }, + ], + }) + + expect(views.map((view) => [view.definition.name, view.rootPath])).toEqual([ + ['Mounted View', '/mounted'], + ['Active View', '/active'], + ]) + }) +}) + +describe('loadMountedVaultFolders', () => { + beforeEach(() => { + mockInvoke.mockReset() + }) + + it('loads folders from the active workspace when folder vaults omit it', async () => { + mockInvoke.mockImplementation((command: string, args?: Record) => { + if (command !== 'list_vault_folders') return Promise.resolve([]) + + const path = String(args?.path) + if (path === '/active') { + return Promise.resolve([{ name: 'active-folder', path: 'active-folder', children: [] }]) + } + if (path === '/mounted') { + return Promise.resolve([{ name: 'mounted-folder', path: 'mounted-folder', children: [] }]) + } + throw new Error(`Unexpected path ${path}`) + }) + + const folders = await loadMountedVaultFolders({ + defaultWorkspacePath: '/mounted', + vaultPath: '/active', + vaults: [ + { label: 'Mounted', path: '/mounted', alias: 'mounted', mounted: true, available: true }, + ], + }) + + expect(folders).toEqual([ + { + name: 'Mounted', + path: '', + rootPath: '/mounted', + children: [{ name: 'mounted-folder', path: 'mounted-folder', rootPath: '/mounted', children: [] }], + }, + { + name: 'active', + path: '', + rootPath: '/active', + children: [{ name: 'active-folder', path: 'active-folder', rootPath: '/active', children: [] }], + }, + ]) + }) + + it('does not duplicate the active workspace when folder vaults already include it', async () => { + mockInvoke.mockImplementation((command: string, args?: Record) => { + if (command !== 'list_vault_folders') return Promise.resolve([]) + return Promise.resolve([{ name: `${String(args?.path)}-folder`, path: 'folder', children: [] }]) + }) + + const folders = await loadMountedVaultFolders({ + defaultWorkspacePath: '/active', + vaultPath: '/active', + vaults: [ + { label: 'Active', path: '/active', alias: 'active', mounted: true, available: true }, + ], + }) + + expect(mockInvoke).toHaveBeenCalledTimes(1) + expect(folders).toEqual([{ name: '/active-folder', path: 'folder', children: [] }]) + }) })