feat(editor): add markdown presets, heading folding and external file refresh
This commit is contained in:
@@ -3,10 +3,13 @@ import { ref, computed } from 'vue'
|
||||
import type { SaveStatus } from '@/contracts'
|
||||
import * as workspaceService from '@/services/workspaceService'
|
||||
import { t } from '@/i18n'
|
||||
import { ApiErrorClass } from '@/services/apiClient'
|
||||
|
||||
export const useEditorStore = defineStore('editor', () => {
|
||||
const mode = ref<'wysiwyg' | 'source'>('wysiwyg')
|
||||
const content = ref('')
|
||||
const contentRevision = ref(0)
|
||||
let diskContent: string | undefined
|
||||
const saveStatus = ref<SaveStatus>('idle')
|
||||
const lastSavedAt = ref<string | null>(null)
|
||||
const currentNoteId = ref<string | null>(null)
|
||||
@@ -35,13 +38,14 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
function updateContent(newContent: string) {
|
||||
content.value = newContent
|
||||
saveStatus.value = 'dirty'
|
||||
if (saveStatus.value !== 'conflict' && saveStatus.value !== 'external_changed') saveStatus.value = 'dirty'
|
||||
}
|
||||
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pendingSave: Promise<void> | null = null
|
||||
|
||||
function scheduleAutoSave(delay = 1500) {
|
||||
if (saveStatus.value === 'conflict' || saveStatus.value === 'external_changed') return
|
||||
if (saveTimer) clearTimeout(saveTimer)
|
||||
saveTimer = setTimeout(() => {
|
||||
saveTimer = null
|
||||
@@ -51,20 +55,23 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
async function save() {
|
||||
if (!currentFilePath.value) return
|
||||
if (saveStatus.value === 'conflict' || saveStatus.value === 'external_changed') return
|
||||
if (pendingSave) return pendingSave
|
||||
// 保存路径与正文都取快照;请求完成时用户可能已继续输入或切换文件。
|
||||
const targetPath = currentFilePath.value
|
||||
const snapshot = content.value
|
||||
const baseline = diskContent
|
||||
saveStatus.value = 'saving'
|
||||
pendingSave = (async () => {
|
||||
try {
|
||||
await workspaceService.saveFileContent(targetPath, snapshot)
|
||||
await workspaceService.saveFileContent(targetPath, snapshot, baseline)
|
||||
if (currentFilePath.value === targetPath) {
|
||||
diskContent = snapshot
|
||||
saveStatus.value = content.value === snapshot ? 'saved' : 'dirty'
|
||||
lastSavedAt.value = new Date().toISOString()
|
||||
}
|
||||
} catch {
|
||||
if (currentFilePath.value === targetPath) saveStatus.value = 'save_failed'
|
||||
} catch (error) {
|
||||
if (currentFilePath.value === targetPath) saveStatus.value = error instanceof ApiErrorClass && error.code === 'NOTE_CONTENT_CONFLICT' ? 'conflict' : 'save_failed'
|
||||
} finally {
|
||||
pendingSave = null
|
||||
if (currentFilePath.value === targetPath && saveStatus.value === 'dirty') scheduleAutoSave()
|
||||
@@ -86,7 +93,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
if (pendingSave) await pendingSave
|
||||
if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed') await save()
|
||||
if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed') {
|
||||
if (['dirty', 'save_failed', 'conflict', 'external_changed'].includes(saveStatus.value)) {
|
||||
throw new Error(t('当前文件保存失败,已阻止切换以避免内容丢失。', 'The current file could not be saved. Switching was blocked to prevent data loss.'))
|
||||
}
|
||||
// 版本号使较慢的旧读取不能覆盖用户后选择的新文件。
|
||||
@@ -102,6 +109,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
currentFilePath.value = filePath
|
||||
currentNoteId.value = loadedNoteId
|
||||
content.value = loadedContent
|
||||
diskContent = loadedContent
|
||||
saveStatus.value = 'saved'
|
||||
lastSavedAt.value = new Date().toISOString()
|
||||
} catch (error) {
|
||||
@@ -122,13 +130,37 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
|
||||
function setExternalChanged() {
|
||||
if (saveStatus.value === 'dirty') {
|
||||
if (saveTimer) { clearTimeout(saveTimer); saveTimer = null }
|
||||
if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed' || saveStatus.value === 'conflict') {
|
||||
saveStatus.value = 'conflict'
|
||||
} else {
|
||||
saveStatus.value = 'external_changed'
|
||||
}
|
||||
}
|
||||
|
||||
async function checkExternalFile() {
|
||||
if (!currentFilePath.value || pendingSave || diskContent === undefined || saveStatus.value === 'conflict') return
|
||||
const path = currentFilePath.value, baseline = diskContent
|
||||
try {
|
||||
const latest = await workspaceService.readFileContent(path)
|
||||
if (path !== currentFilePath.value || pendingSave || diskContent !== baseline || latest === baseline) return
|
||||
if (content.value === baseline && saveStatus.value === 'saved') {
|
||||
content.value = latest; diskContent = latest; contentRevision.value++
|
||||
} else {
|
||||
setExternalChanged(); saveStatus.value = 'conflict'
|
||||
}
|
||||
} catch { /* Tree polling reports missing files; transient network errors retain edits. */ }
|
||||
}
|
||||
|
||||
async function reloadExternalFile() {
|
||||
const path = currentFilePath.value, snapshot = content.value
|
||||
if (!path || pendingSave) return
|
||||
const latest = await workspaceService.readFileContent(path)
|
||||
if (path !== currentFilePath.value || content.value !== snapshot || pendingSave) return
|
||||
if (saveTimer) { clearTimeout(saveTimer); saveTimer = null }
|
||||
content.value = latest; diskContent = latest; saveStatus.value = 'saved'; contentRevision.value++
|
||||
}
|
||||
|
||||
// TODO(editor): 桌面文件监听接入后提供冲突对比/合并界面,而非只阻止切换。
|
||||
|
||||
function closeFile() {
|
||||
@@ -137,6 +169,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
currentFilePath.value = null
|
||||
currentNoteId.value = null
|
||||
content.value = ''
|
||||
diskContent = undefined
|
||||
saveStatus.value = 'idle'
|
||||
lastSavedAt.value = null
|
||||
highlightBlockId.value = null
|
||||
@@ -153,6 +186,9 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
jumpToHeading,
|
||||
mode,
|
||||
content,
|
||||
contentRevision,
|
||||
checkExternalFile,
|
||||
reloadExternalFile,
|
||||
saveStatus,
|
||||
lastSavedAt,
|
||||
currentNoteId,
|
||||
|
||||
@@ -6,6 +6,30 @@ import * as workspace from '@/services/workspaceService'
|
||||
|
||||
afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers() })
|
||||
|
||||
it('reloads clean external changes but preserves unsaved edits and blocks overwrite', async () => {
|
||||
setActivePinia(createPinia())
|
||||
vi.spyOn(workspace, 'getNoteId').mockResolvedValue('id')
|
||||
const read = vi.spyOn(workspace, 'readFileContent').mockResolvedValue('original')
|
||||
const write = vi.spyOn(workspace, 'saveFileContent').mockResolvedValue()
|
||||
const store = useEditorStore()
|
||||
await store.loadFile('/draft.md')
|
||||
read.mockResolvedValue('external')
|
||||
await store.checkExternalFile()
|
||||
expect(store.content).toBe('external')
|
||||
expect(store.contentRevision).toBe(1)
|
||||
store.updateContent('my unsaved changes')
|
||||
read.mockResolvedValue('new external')
|
||||
await store.checkExternalFile()
|
||||
expect(store.saveStatus).toBe('conflict')
|
||||
expect(store.content).toBe('my unsaved changes')
|
||||
store.updateContent('keep editing')
|
||||
await store.save()
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
await store.reloadExternalFile()
|
||||
expect(store.content).toBe('new external')
|
||||
expect(store.saveStatus).toBe('saved')
|
||||
})
|
||||
|
||||
it('saves text typed while the previous save is still pending', async () => {
|
||||
vi.useFakeTimers()
|
||||
setActivePinia(createPinia())
|
||||
@@ -20,6 +44,6 @@ it('saves text typed while the previous save is still pending', async () => {
|
||||
await saving
|
||||
expect(store.saveStatus).toBe('dirty')
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
expect(write).toHaveBeenLastCalledWith('/draft.md', 'latest')
|
||||
expect(write).toHaveBeenLastCalledWith('/draft.md', 'latest', 'first')
|
||||
expect(store.saveStatus).toBe('saved')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { beforeEach, expect, it } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { normalizeHeadingAppearance, useHeadingAppearanceStore } from './headingAppearance'
|
||||
beforeEach(() => { localStorage.clear(); setActivePinia(createPinia()) })
|
||||
it('persists heading preferences and restores theme defaults without residual overrides', async () => {
|
||||
const store = useHeadingAppearanceStore()
|
||||
expect(store.cssVariables).toEqual({})
|
||||
store.preferences.custom = true
|
||||
store.preferences.levels[1]!.size = 35
|
||||
store.preferences.levels[1]!.weight = 400
|
||||
await nextTick()
|
||||
setActivePinia(createPinia())
|
||||
const restored = useHeadingAppearanceStore()
|
||||
expect(restored.cssVariables['--heading-2-size']).toBe('35px')
|
||||
expect(restored.cssVariables['--heading-2-weight']).toBe('400')
|
||||
restored.reset()
|
||||
expect(restored.cssVariables).toEqual({})
|
||||
})
|
||||
it('rejects invalid storage and limits values before applying CSS', () => {
|
||||
localStorage.setItem('editor-heading-appearance', 'invalid')
|
||||
expect(useHeadingAppearanceStore().preferences.custom).toBe(false)
|
||||
const result = normalizeHeadingAppearance({ custom: true, family: 'url(unsafe)', levels: [{ size: 9999, weight: 2 }, { size: NaN }] })
|
||||
expect(result.family).toBe('inherit')
|
||||
expect(result.levels[0]).toEqual({ size: 72, weight: 700 })
|
||||
expect(result.levels[1]!.size).toBe(28)
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import '@/styles/headings.css'
|
||||
|
||||
const key = 'editor-heading-appearance'
|
||||
export const defaultHeadingSizes = [32, 28, 24, 21, 18, 16]
|
||||
export function normalizeHeadingAppearance(value: unknown) {
|
||||
const raw = value && typeof value === 'object' ? value as Record<string, unknown> : {}
|
||||
const levels = Array.isArray(raw.levels) ? raw.levels : []
|
||||
return {
|
||||
custom: raw.custom === true,
|
||||
family: ['inherit', 'serif', 'sans-serif', 'monospace'].includes(String(raw.family)) ? String(raw.family) : 'inherit',
|
||||
levels: defaultHeadingSizes.map((size, index) => {
|
||||
const item = levels[index] ?? {}
|
||||
return {
|
||||
size: typeof item.size === 'number' && Number.isFinite(item.size) ? Math.min(72, Math.max(12, item.size)) : size,
|
||||
weight: [400, 500, 600, 700, 800].includes(item.weight) ? Number(item.weight) : 700,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const useHeadingAppearanceStore = defineStore('heading-appearance', () => {
|
||||
let saved: unknown
|
||||
try { saved = JSON.parse(localStorage.getItem(key) ?? '{}') } catch { saved = {} }
|
||||
const preferences = ref(normalizeHeadingAppearance(saved))
|
||||
watch(preferences, value => localStorage.setItem(key, JSON.stringify(normalizeHeadingAppearance(value))), { deep: true })
|
||||
const cssVariables = computed(() => {
|
||||
const normalized = normalizeHeadingAppearance(preferences.value)
|
||||
if (!normalized.custom) return {}
|
||||
const result: Record<string, string> = { '--heading-family': normalized.family }
|
||||
normalized.levels.forEach((item, index) => {
|
||||
result[`--heading-${index + 1}-size`] = `${item.size}px`
|
||||
result[`--heading-${index + 1}-weight`] = String(item.weight)
|
||||
})
|
||||
return result
|
||||
})
|
||||
function reset() { preferences.value = normalizeHeadingAppearance({}) }
|
||||
return { preferences, cssVariables, reset }
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { beforeEach, expect, it } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { useMarkdownPreferencesStore, markdownPresets } from './markdownPreferences'
|
||||
beforeEach(() => { localStorage.clear(); setActivePinia(createPinia()) })
|
||||
it('saves, replaces and restores named syntax presets', async () => {
|
||||
const store = useMarkdownPreferencesStore()
|
||||
store.preferences.heading = 'setext'
|
||||
store.preferences.bullet = '+'
|
||||
expect(store.savePreset('我的格式')).toBe(true)
|
||||
store.apply(markdownPresets.plain)
|
||||
expect(store.normalized.callouts).toBe(false)
|
||||
store.apply(store.customPresets[0]!.preferences)
|
||||
expect(store.normalized.heading).toBe('setext')
|
||||
await nextTick()
|
||||
setActivePinia(createPinia())
|
||||
expect(useMarkdownPreferencesStore().normalized.bullet).toBe('+')
|
||||
expect(useMarkdownPreferencesStore().customPresets[0]!.name).toBe('我的格式')
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
export interface MarkdownPreferences {
|
||||
heading: 'atx' | 'setext'; bullet: '-' | '*' | '+'; incrementList: boolean
|
||||
fence: '`' | '~'; math: boolean; callouts: boolean; diagrams: boolean; autoLinks: boolean
|
||||
lineNumbers: boolean; wrapCode: boolean; indent: number; defaultLanguage: string
|
||||
}
|
||||
export const defaultMarkdownPreferences: MarkdownPreferences = {
|
||||
heading: 'atx', bullet: '-', incrementList: true, fence: '`', math: true, callouts: true,
|
||||
diagrams: true, autoLinks: true, lineNumbers: true, wrapCode: false, indent: 4, defaultLanguage: '',
|
||||
}
|
||||
export function normalizeMarkdownPreferences(value: unknown): MarkdownPreferences {
|
||||
const raw = value && typeof value === 'object' ? value as Record<string, unknown> : {}
|
||||
const result = { ...defaultMarkdownPreferences }
|
||||
for (const key of ['incrementList','math','callouts','diagrams','autoLinks','lineNumbers','wrapCode'] as const) if (typeof raw[key] === 'boolean') result[key] = raw[key]
|
||||
result.heading = raw.heading === 'setext' ? 'setext' : 'atx'
|
||||
result.bullet = raw.bullet === '*' || raw.bullet === '+' ? raw.bullet : '-'
|
||||
result.fence = raw.fence === '~' ? '~' : '`'
|
||||
result.indent = [2,4,8].includes(Number(raw.indent)) ? Number(raw.indent) : 4
|
||||
result.defaultLanguage = typeof raw.defaultLanguage === 'string' && /^[\w+-]{0,40}$/.test(raw.defaultLanguage) ? raw.defaultLanguage : ''
|
||||
return result
|
||||
}
|
||||
export const markdownPresets = {
|
||||
extended: defaultMarkdownPreferences,
|
||||
github: { ...defaultMarkdownPreferences, math: false },
|
||||
plain: { ...defaultMarkdownPreferences, math: false, callouts: false, diagrams: false, autoLinks: false },
|
||||
}
|
||||
const key = 'markdown-preferences'
|
||||
export const useMarkdownPreferencesStore = defineStore('markdown-preferences', () => {
|
||||
let saved: Record<string, unknown> = {}
|
||||
try { saved = JSON.parse(localStorage.getItem(key) ?? '{}') ?? {} } catch { /* defaults */ }
|
||||
const preferences = ref(normalizeMarkdownPreferences(saved.preferences))
|
||||
const customPresets = ref<{ name: string; preferences: MarkdownPreferences }[]>(Array.isArray(saved.presets)
|
||||
? saved.presets.filter(item => item && typeof item.name === 'string').slice(0, 20).map(item => ({ name: item.name.slice(0, 40), preferences: normalizeMarkdownPreferences(item.preferences) })) : [])
|
||||
const normalized = computed(() => normalizeMarkdownPreferences(preferences.value))
|
||||
watch([preferences, customPresets], () => localStorage.setItem(key, JSON.stringify({ preferences: normalized.value, presets: customPresets.value })), { deep: true })
|
||||
function apply(value: unknown) { preferences.value = normalizeMarkdownPreferences(value) }
|
||||
function savePreset(name: string) {
|
||||
name = name.trim().slice(0, 40)
|
||||
if (!name) return false
|
||||
const existing = customPresets.value.find(item => item.name === name)
|
||||
if (existing) existing.preferences = { ...normalized.value }
|
||||
else if (customPresets.value.length < 20) customPresets.value.push({ name, preferences: { ...normalized.value } })
|
||||
else return false
|
||||
return true
|
||||
}
|
||||
return { preferences, normalized, customPresets, apply, savePreset }
|
||||
})
|
||||
@@ -1,13 +1,14 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useHeadingAppearanceStore } from './headingAppearance'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { ThemeConfig, ThemeManifest, InstalledTheme, ThemePackageInspection } from '@/contracts'
|
||||
import * as themePkg from '@/services/themePackageService'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const builtinThemes = (): ThemeConfig[] => [
|
||||
{ theme_id: 'light', name: t('浅色', 'Light'), version: '1.2.0', description: t('默认浅色主题', 'Default light theme'), is_dark: false, builtin: true, code_theme: 'github-light' },
|
||||
{ theme_id: 'dark', name: t('深色', 'Dark'), version: '1.2.0', description: t('默认深色主题', 'Default dark theme'), is_dark: true, builtin: true, code_theme: 'github-dark' },
|
||||
{ theme_id: 'sepia', name: t('护眼', 'Sepia'), version: '1.2.0', description: t('护眼暖色调', 'Warm, low-glare theme'), is_dark: false, builtin: true, code_theme: 'github-light' },
|
||||
{ theme_id: 'light', name: t('浅色', 'Light'), version: '1.3.0', description: t('默认浅色主题', 'Default light theme'), is_dark: false, builtin: true, code_theme: 'github-light' },
|
||||
{ theme_id: 'dark', name: t('深色', 'Dark'), version: '1.3.0', description: t('默认深色主题', 'Default dark theme'), is_dark: true, builtin: true, code_theme: 'github-dark' },
|
||||
{ theme_id: 'sepia', name: t('护眼', 'Sepia'), version: '1.3.0', description: t('护眼暖色调', 'Warm, low-glare theme'), is_dark: false, builtin: true, code_theme: 'github-light' },
|
||||
]
|
||||
|
||||
export type CodeBlockThemePreference = 'auto' | 'github-light' | 'github-dark'
|
||||
@@ -158,6 +159,7 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
useHeadingAppearanceStore().reset()
|
||||
applyTheme('light')
|
||||
fontEditorSize.value = 15
|
||||
fontEditorFamily.value = 'system-ui'
|
||||
|
||||
@@ -13,6 +13,8 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
const isLoading = ref(false)
|
||||
const hasVault = ref(false)
|
||||
const recentVaults = ref<workspaceService.VaultInfo[]>([])
|
||||
const treeRefreshError = ref<string | null>(null)
|
||||
let refreshSequence = 0
|
||||
|
||||
const activeFile = computed(() => {
|
||||
if (!activeFilePath.value) return null
|
||||
@@ -65,10 +67,27 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
/** 重新拉取文件树。插件命令返回 refresh:workspace 时需要。 */
|
||||
async function refreshFileTree() {
|
||||
if (!hasVault.value) return
|
||||
fileTree.value = await workspaceService.getFileTree()
|
||||
const sequence = ++refreshSequence
|
||||
const path = vaultPath.value
|
||||
try {
|
||||
const fresh = await workspaceService.refreshTree()
|
||||
if (sequence !== refreshSequence || path !== vaultPath.value || !hasVault.value) return
|
||||
const open = new Map<string, boolean>()
|
||||
const collect = (nodes: FileNode[]) => nodes.forEach(node => { if (node.type === 'folder') open.set(node.path, !!node.is_open); if (node.children) collect(node.children) })
|
||||
collect(fileTree.value)
|
||||
const restore = (nodes: FileNode[]) => nodes.forEach(node => { if (node.type === 'folder') node.is_open = open.get(node.path) ?? false; if (node.children) restore(node.children) })
|
||||
restore(fresh)
|
||||
// Avoid redrawing an unchanged tree on every background check.
|
||||
if (JSON.stringify(fresh) !== JSON.stringify(fileTree.value)) fileTree.value = fresh
|
||||
treeRefreshError.value = null
|
||||
} catch (error) {
|
||||
if (sequence === refreshSequence) treeRefreshError.value = error instanceof Error ? error.message : '文件树刷新失败'
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function openVault(path: string) {
|
||||
refreshSequence++
|
||||
isLoading.value = true
|
||||
try {
|
||||
const info = await workspaceService.openVault(path)
|
||||
@@ -84,6 +103,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
}
|
||||
|
||||
async function createVault(path: string, name: string) {
|
||||
refreshSequence++
|
||||
isLoading.value = true
|
||||
try {
|
||||
const info = await workspaceService.createVault(path, name)
|
||||
@@ -162,6 +182,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
isLoading,
|
||||
hasVault,
|
||||
recentVaults,
|
||||
treeRefreshError,
|
||||
toggleFolder,
|
||||
openFile,
|
||||
closeFile,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useWorkspaceStore } from './workspace'
|
||||
import * as service from '@/services/workspaceService'
|
||||
beforeEach(() => { setActivePinia(createPinia()); vi.restoreAllMocks() })
|
||||
it('fetches external entries while keeping folder state and ignoring stale responses', async () => {
|
||||
const store = useWorkspaceStore()
|
||||
store.hasVault = true; store.vaultPath = '/vault'
|
||||
store.fileTree = [{ id: 'folder', path: '/folder', name: 'folder', type: 'folder', is_open: true, children: [] }]
|
||||
let release!: (value: typeof store.fileTree) => void
|
||||
vi.spyOn(service, 'refreshTree').mockImplementationOnce(() => new Promise(resolve => { release = resolve }))
|
||||
.mockResolvedValueOnce([{ id: 'folder', path: '/folder', name: 'folder', type: 'folder', children: [{ id: 'new', path: '/folder/new.md', name: 'new.md', type: 'file' }] }])
|
||||
const old = store.refreshFileTree()
|
||||
await store.refreshFileTree()
|
||||
expect(store.fileTree[0]!.is_open).toBe(true)
|
||||
expect(store.fileTree[0]!.children).toHaveLength(1)
|
||||
release([]); await old
|
||||
expect(store.fileTree).toHaveLength(1)
|
||||
})
|
||||
Reference in New Issue
Block a user