// 文件树回归:子文件夹展开、空文件夹提示、文件夹右键新建文档/文件夹。 import { describe, it, expect, vi } from 'vitest' import { createRoot } from 'react-dom/client' import { act } from 'react-dom/test-utils' vi.mock('../src/services/platform', async (importOriginal) => { const orig = await importOriginal() return { ...orig, listDir: async (dir: string) => { if (dir === 'C:/ws') { return [ { name: 'sub', path: 'C:/ws/sub', isDir: true }, { name: 'empty', path: 'C:/ws/empty', isDir: true }, { name: 'a.md', path: 'C:/ws/a.md', isDir: false }, ] } if (dir === 'C:/ws/sub') return [{ name: 'b.md', path: 'C:/ws/sub/b.md', isDir: false }] return [] }, } }) import Sidebar from '../src/components/Sidebar' const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) async function renderSidebar(extra: Record = {}) { const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) await act(async () => { root.render( undefined} workspaceRoot="C:/ws" activePath={null} onOpenFile={() => undefined} onOpenFolder={() => undefined} outline={[]} onJumpHeading={() => undefined} tauriMode {...extra} />, ) }) await act(async () => { await sleep(30) }) return { container, root } } function findButton(container: HTMLElement, text: string) { return [...container.querySelectorAll('button')].find((b) => b.textContent?.includes(text)) } describe('文件树', () => { it('子文件夹点击展开能看到下级文件', async () => { const { container, root } = await renderSidebar() await act(async () => { findButton(container, 'sub')?.dispatchEvent(new MouseEvent('click', { bubbles: true })) }) await act(async () => { await sleep(30) }) expect(container.innerHTML).toContain('b.md') await act(async () => { root.unmount() }) container.remove() }) it('空文件夹展开后有(空)提示而不是空白', async () => { const { container, root } = await renderSidebar() await act(async () => { findButton(container, 'empty')?.dispatchEvent(new MouseEvent('click', { bubbles: true })) }) await act(async () => { await sleep(30) }) expect(container.innerHTML).toContain('(空)') await act(async () => { root.unmount() }) container.remove() }) it('文件夹右键菜单有新建文档/文件夹并回传该文件夹', async () => { const created: string[] = [] const folders: string[] = [] const { container, root } = await renderSidebar({ onCreateFileIn: (dir: string) => created.push(dir), onCreateFolderIn: (dir: string) => folders.push(dir), }) await act(async () => { findButton(container, 'sub')?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true })) }) await act(async () => { await sleep(10) }) const fileItem = findButton(container, '在此新建文档') const folderItem = findButton(container, '在此新建文件夹') expect(fileItem).toBeTruthy() expect(folderItem).toBeTruthy() await act(async () => { fileItem?.dispatchEvent(new MouseEvent('click', { bubbles: true })) }) expect(created).toEqual(['C:/ws/sub']) await act(async () => { root.unmount() }) container.remove() }) }) describe('文件树拖拽移动', () => { const fireDrag = async (el: Element | undefined | null, type: string) => { if (!el) throw new Error(`元素不存在,无法派发 ${type}`) const e = new Event(type, { bubbles: true, cancelable: true }) as Event & { dataTransfer: Record } e.dataTransfer = { effectAllowed: '', dropEffect: '', setData: () => undefined, getData: () => '' } await act(async () => { el.dispatchEvent(e) }) } it('把文件拖到文件夹上触发移动回调', async () => { const moves: { path: string; dest: string }[] = [] const { container, root } = await renderSidebar({ onMoveEntry: (entry: { path: string }, dest: string) => moves.push({ path: entry.path, dest }), }) const file = findButton(container, 'a.md') const folder = findButton(container, 'sub') await fireDrag(file, 'dragstart') await fireDrag(folder, 'dragover') expect(folder?.className).toContain('drop-target') await fireDrag(folder, 'drop') expect(moves).toEqual([{ path: 'C:/ws/a.md', dest: 'C:/ws/sub' }]) await act(async () => { root.unmount() }) container.remove() }) it('拖回原文件夹(根目录)不触发移动', async () => { const moves: unknown[] = [] const { container, root } = await renderSidebar({ onMoveEntry: (...args: unknown[]) => moves.push(args), }) const file = findButton(container, 'a.md') const head = container.querySelector('.filetree-head') await fireDrag(file, 'dragstart') await fireDrag(head, 'dragover') await fireDrag(head, 'drop') expect(moves).toEqual([]) await act(async () => { root.unmount() }) container.remove() }) it('子文件夹里的文件可以拖回根目录', async () => { const moves: { path: string; dest: string }[] = [] const { container, root } = await renderSidebar({ onMoveEntry: (entry: { path: string }, dest: string) => moves.push({ path: entry.path, dest }), }) await act(async () => { findButton(container, 'sub')?.dispatchEvent(new MouseEvent('click', { bubbles: true })) }) await act(async () => { await sleep(30) }) const inner = findButton(container, 'b.md') const head = container.querySelector('.filetree-head') await fireDrag(inner, 'dragstart') await fireDrag(head, 'dragover') await fireDrag(head, 'drop') expect(moves).toEqual([{ path: 'C:/ws/sub/b.md', dest: 'C:/ws' }]) await act(async () => { root.unmount() }) container.remove() }) it('文件夹拖到自己身上不触发移动', async () => { const moves: unknown[] = [] const { container, root } = await renderSidebar({ onMoveEntry: (...args: unknown[]) => moves.push(args), }) const folder = findButton(container, 'sub') await fireDrag(folder, 'dragstart') await fireDrag(folder, 'dragover') await fireDrag(folder, 'drop') expect(moves).toEqual([]) await act(async () => { root.unmount() }) container.remove() }) it('右键菜单含「打开所在位置」并回传条目', async () => { const revealed: string[] = [] const { container, root } = await renderSidebar({ onRevealEntry: (entry: { path: string }) => revealed.push(entry.path), }) await act(async () => { findButton(container, 'a.md')?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true })) }) await act(async () => { await sleep(10) }) const item = findButton(container, '打开所在位置') expect(item).toBeTruthy() await act(async () => { item?.dispatchEvent(new MouseEvent('click', { bubbles: true })) }) expect(revealed).toEqual(['C:/ws/a.md']) await act(async () => { root.unmount() }) container.remove() }) })