// 渲染端 showFileListContextMenu 的「在文件夹中新建」项: // 右键文件夹条目时,菜单顶部应出现「在文件夹中新建文件 / 新建文件夹」, // 且选中后路由到 createNewFile / createNewFolder 并带上被右击文件夹的绝对路径 // 作为落点(targetDir)。文件条目则不应出现这组项。 // // 同时覆盖 createNewFile/createNewFolder 的 targetDir 透传: // 传 folder path 时主进程 createFile/createFolder 的 dir 等于该 path。 // @vitest-environment jsdom import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createFileOps } from '../../src/file-ops.js'; // 桩 prompt / confirm(createNewFile 内部会弹,返回默认值即可跑通 IPC 路径)。 // confirm 仅在 state.isDirty 时触发,本测试 state.isDirty 恒为 false,不会走到。 vi.mock('../../src/prompt-dialog.js', () => ({ showPromptDialog: async ({ defaultValue }) => defaultValue, })); vi.mock('../../src/confirm-dialog.js', () => ({ showConfirmDialog: async () => 'confirm', })); function makeFileOps(apiCalls) { const contextMenu = { show: vi.fn() }; const state = { currentFile: null, isDirty: false, breadcrumb: [] }; const api = { createFile: vi.fn(async (name, opts) => { apiCalls.createFile = { name, opts }; return { ok: true, name, path: `/d/${name}`, mtimeMs: 1, size: 1 }; }), createFolder: vi.fn(async (name, opts) => { apiCalls.createFolder = { name, opts }; return { ok: true, name, path: `/d/${name}`, mtimeMs: 1, size: 1 }; }), }; const ops = createFileOps({ state, controls: { fileList: {}, viewer: {}, contextMenu, editor: { isDirty: () => false, getContent: () => '' }, }, api, callbacks: { openFile: vi.fn(), updateWindowTitle: vi.fn(), clearCurrentFile: vi.fn(), updateStatusStats: vi.fn(), syncSaveButton: vi.fn(), applyEditorMode: vi.fn(), }, }); return { ops, contextMenu }; } describe('showFileListContextMenu 在文件夹中新建', () => { let apiCalls; let ops; let contextMenu; beforeEach(() => { apiCalls = {}; ({ ops, contextMenu } = makeFileOps(apiCalls)); }); it('文件夹条目:顶部含「在文件夹中新建文件 / 新建文件夹」+ 分隔条', () => { ops.showFileListContextMenu({ name: 'sub', path: '/d/sub', entryType: 'folder' }, 10, 10); expect(contextMenu.show).toHaveBeenCalledTimes(1); const items = contextMenu.show.mock.calls[0][0].items; const labels = items.map((i) => i.label); expect(labels[0]).toBe('在文件夹中新建文件'); expect(labels[1]).toBe('在文件夹中新建文件夹'); expect(items[2]).toMatchObject({ separator: true }); // 既有项仍在 expect(labels).toContain('重命名'); expect(labels).toContain('删除'); expect(labels).toContain('粘贴'); }); it('文件条目:不出现「在文件夹中新建」项', () => { ops.showFileListContextMenu({ name: 'a.md', path: '/d/a.md', entryType: 'editable' }, 10, 10); const labels = contextMenu.show.mock.calls[0][0].items.map((i) => i.label); expect(labels).not.toContain('在文件夹中新建文件'); expect(labels).not.toContain('在文件夹中新建文件夹'); }); it('文件夹右键「在文件夹中新建文件」→ createNewFile 落在该文件夹', async () => { ops.showFileListContextMenu({ name: 'sub', path: '/d/sub', entryType: 'folder' }, 10, 10); const { onSelect, items } = contextMenu.show.mock.calls[0][0]; const item = items.find((i) => i.value === 'new_file_in'); expect(item).toBeTruthy(); await onSelect('new_file_in'); expect(apiCalls.createFile).toBeDefined(); expect(apiCalls.createFile.opts.dir).toBe('/d/sub'); }); it('文件夹右键「在文件夹中新建文件夹」→ createNewFolder 落在该文件夹', async () => { ops.showFileListContextMenu({ name: 'sub', path: '/d/sub', entryType: 'folder' }, 10, 10); const { onSelect } = contextMenu.show.mock.calls[0][0]; await onSelect('new_folder_in'); expect(apiCalls.createFolder).toBeDefined(); expect(apiCalls.createFolder.opts.dir).toBe('/d/sub'); }); }); describe('createNewFile / createNewFolder targetDir 透传', () => { it('createNewFile 不传 targetDir → dir 落当前浏览目录', async () => { const apiCalls = {}; const { ops } = makeFileOps(apiCalls); // state.currentDir 默认空串 → dir 应为 ''(与旧行为一致) await ops.createNewFile(); expect(apiCalls.createFile.opts.dir).toBe(''); }); it('createNewFile 传 targetDir → dir 等于该路径', async () => { const apiCalls = {}; const { ops } = makeFileOps(apiCalls); await ops.createNewFile('/d/sub'); expect(apiCalls.createFile.opts.dir).toBe('/d/sub'); }); it('createNewFolder 传 targetDir → dir 等于该路径', async () => { const apiCalls = {}; const { ops } = makeFileOps(apiCalls); await ops.createNewFolder('/d/sub'); expect(apiCalls.createFolder.opts.dir).toBe('/d/sub'); }); });