fix: tighten pulled note refresh flow
This commit is contained in:
90
src/utils/pulledVaultRefresh.test.ts
Normal file
90
src/utils/pulledVaultRefresh.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { refreshPulledVaultState } from './pulledVaultRefresh'
|
||||
|
||||
function makeEntry(path: string, title = 'Test note'): VaultEntry {
|
||||
return {
|
||||
path,
|
||||
title,
|
||||
filename: path.split('/').pop() ?? 'note.md',
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
outgoingLinks: [],
|
||||
} as VaultEntry
|
||||
}
|
||||
|
||||
function makeOptions(overrides: Partial<Parameters<typeof refreshPulledVaultState>[0]> = {}) {
|
||||
const activeEntry = makeEntry('/vault/active.md', 'Active')
|
||||
return {
|
||||
activeTabPath: activeEntry.path,
|
||||
closeAllTabs: vi.fn(),
|
||||
hasUnsavedChanges: vi.fn(() => false),
|
||||
reloadFolders: vi.fn(),
|
||||
reloadVault: vi.fn().mockResolvedValue([activeEntry]),
|
||||
reloadViews: vi.fn(),
|
||||
replaceActiveTab: vi.fn().mockResolvedValue(undefined),
|
||||
updatedFiles: ['active.md'],
|
||||
vaultPath: '/vault',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('refreshPulledVaultState', () => {
|
||||
it('reloads vault-derived data and refreshes the active note when pull updated it', async () => {
|
||||
const options = makeOptions()
|
||||
|
||||
const entries = await refreshPulledVaultState(options)
|
||||
|
||||
expect(entries).toHaveLength(1)
|
||||
expect(options.reloadVault).toHaveBeenCalledOnce()
|
||||
expect(options.reloadFolders).toHaveBeenCalledOnce()
|
||||
expect(options.reloadViews).toHaveBeenCalledOnce()
|
||||
expect(options.replaceActiveTab).toHaveBeenCalledWith(entries[0])
|
||||
expect(options.closeAllTabs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reloads the active tab after any successful pull with updates', async () => {
|
||||
const options = makeOptions({ updatedFiles: ['project/plan.md'] })
|
||||
|
||||
await refreshPulledVaultState(options)
|
||||
|
||||
expect(options.reloadVault).toHaveBeenCalledOnce()
|
||||
expect(options.replaceActiveTab).toHaveBeenCalledWith(expect.objectContaining({ path: '/vault/active.md' }))
|
||||
expect(options.closeAllTabs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('matches macOS /tmp and /private/tmp aliases when reloading the active tab entry', async () => {
|
||||
const activeEntry = makeEntry('/private/tmp/tolaria/active.md', 'Active')
|
||||
const options = makeOptions({
|
||||
activeTabPath: activeEntry.path,
|
||||
reloadVault: vi.fn().mockResolvedValue([activeEntry]),
|
||||
vaultPath: '/tmp/tolaria',
|
||||
})
|
||||
|
||||
await refreshPulledVaultState(options)
|
||||
|
||||
expect(options.replaceActiveTab).toHaveBeenCalledWith(activeEntry)
|
||||
})
|
||||
|
||||
it('skips tab replacement when the active note has unsaved edits', async () => {
|
||||
const options = makeOptions({
|
||||
hasUnsavedChanges: vi.fn((path: string) => path === '/vault/active.md'),
|
||||
})
|
||||
|
||||
await refreshPulledVaultState(options)
|
||||
|
||||
expect(options.replaceActiveTab).not.toHaveBeenCalled()
|
||||
expect(options.closeAllTabs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('closes the tab when the pulled note disappeared from the reloaded vault', async () => {
|
||||
const options = makeOptions({
|
||||
reloadVault: vi.fn().mockResolvedValue([makeEntry('/vault/other.md', 'Other')]),
|
||||
})
|
||||
|
||||
await refreshPulledVaultState(options)
|
||||
|
||||
expect(options.replaceActiveTab).not.toHaveBeenCalled()
|
||||
expect(options.closeAllTabs).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
49
src/utils/pulledVaultRefresh.ts
Normal file
49
src/utils/pulledVaultRefresh.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { VaultEntry } from '../types'
|
||||
|
||||
interface PulledVaultRefreshOptions {
|
||||
activeTabPath: string | null
|
||||
closeAllTabs: () => void
|
||||
hasUnsavedChanges: (path: string) => boolean
|
||||
reloadFolders: () => Promise<unknown> | unknown
|
||||
reloadVault: () => Promise<VaultEntry[]>
|
||||
reloadViews: () => Promise<unknown> | unknown
|
||||
replaceActiveTab: (entry: VaultEntry) => Promise<void>
|
||||
updatedFiles: string[]
|
||||
vaultPath: string
|
||||
}
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
return path
|
||||
.replaceAll('\\', '/')
|
||||
.replace(/^\/private\/tmp(?=\/|$)/u, '/tmp')
|
||||
.replace(/\/+$/u, '')
|
||||
}
|
||||
|
||||
export async function refreshPulledVaultState(options: PulledVaultRefreshOptions): Promise<VaultEntry[]> {
|
||||
const {
|
||||
activeTabPath,
|
||||
closeAllTabs,
|
||||
hasUnsavedChanges,
|
||||
reloadFolders,
|
||||
reloadVault,
|
||||
reloadViews,
|
||||
replaceActiveTab,
|
||||
} = options
|
||||
|
||||
const [entries] = await Promise.all([
|
||||
reloadVault(),
|
||||
Promise.resolve(reloadFolders()),
|
||||
Promise.resolve(reloadViews()),
|
||||
])
|
||||
|
||||
if (!activeTabPath || hasUnsavedChanges(activeTabPath)) return entries
|
||||
|
||||
const refreshedEntry = entries.find(entry => normalizePath(entry.path) === normalizePath(activeTabPath))
|
||||
if (!refreshedEntry) {
|
||||
closeAllTabs()
|
||||
return entries
|
||||
}
|
||||
|
||||
await replaceActiveTab(refreshedEntry)
|
||||
return entries
|
||||
}
|
||||
Reference in New Issue
Block a user