Files
MyNote/tests/unit/file-service.test.ts
T

145 lines
5.4 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import nodePath from 'node:path';
import os from 'node:os';
import { promises as fs } from 'node:fs';
import { FileService } from '../../apps/desktop/src/main/services/file-service';
import { ConfigStore } from '../../apps/desktop/src/main/services/config-store';
const tmp = nodePath.join(os.tmpdir(), 'dsh-file-service-test');
beforeEach(async () => { await fs.rm(tmp, { recursive: true, force: true }); await fs.mkdir(tmp, { recursive: true }); });
afterEach(async () => { await fs.rm(tmp, { recursive: true, force: true }); });
describe('FileService CRUD (FR-F)', () => {
it('create 文档生成 .md', async () => {
const s = new FileService(tmp);
const { path } = await s.create('', '笔记', false);
expect(path).toBe('笔记.md');
expect(await fs.readFile(nodePath.join(tmp, '笔记.md'), 'utf8')).toBe('');
});
it('create 文件夹', async () => {
const s = new FileService(tmp);
const { path } = await s.create('', '目录', true);
expect(path).toBe('目录');
expect((await fs.stat(nodePath.join(tmp, '目录'))).isDirectory()).toBe(true);
});
it('rename 文档自动补 .md 并去重', async () => {
const s = new FileService(tmp);
await s.create('', 'a', false);
await s.create('', 'b', false);
const { path } = await s.rename('b.md', 'a');
expect(path).toBe('a-1.md');
});
it('move 到目标目录', async () => {
const s = new FileService(tmp);
await s.create('', 'dir', true);
await s.create('', 'x', false);
const { path } = await s.move('x.md', 'dir');
expect(path).toBe('dir/x.md');
});
it('move 禁止移入自身内部', async () => {
const s = new FileService(tmp);
await s.create('', 'dir', true);
await s.create('', 'dir/sub', true);
await expect(s.move('dir', 'dir/sub')).rejects.toThrow();
});
it('delete 文档与递归文件夹', async () => {
const s = new FileService(tmp);
await s.create('', 'f', true);
await s.create('f', 'inner', false);
await s.delete('f', true);
await expect(fs.stat(nodePath.join(tmp, 'f'))).rejects.toThrow();
});
it('listTree 排序:文件夹在前、忽略隐藏目录', async () => {
const s = new FileService(tmp);
await s.create('', 'zdoc', false);
await s.create('', 'afolder', true);
await s.create('', '.hidden', true);
await s.create('afolder', 'in', false);
const tree = await s.listTree();
const folders = tree.filter(n => n.type === 'folder').map(n => n.name);
const docs = tree.filter(n => n.type === 'doc' && n.parent === '').map(n => n.name);
expect(folders[0]).toBe('afolder');
expect(docs).toContain('zdoc');
expect(folders).not.toContain('.hidden');
});
it('importFiles 复制并去重,返回成功与失败', async () => {
const s = new FileService(tmp);
const ext = nodePath.join(tmp, '_external');
await fs.mkdir(ext, { recursive: true });
await fs.writeFile(nodePath.join(ext, 'p.md'), 'hi');
const r = await s.importFiles([nodePath.join(ext, 'p.md'), nodePath.join(ext, 'nope.md')], '');
expect(r.ok).toEqual(['p.md']);
expect(r.failed.length).toBe(1);
});
});
describe('FileService 金标准:writeDoc → readDoc 往返字节一致 (FR-S5/FR-M1)', () => {
it('纯正文往返', async () => {
const s = new FileService(tmp);
await s.create('', 'doc', false);
const body = '# 标题\n\n段落含 **粗** 与 `code`。';
await s.writeDoc({ path: 'doc.md', meta: {}, body });
const r = await s.readDoc('doc.md');
expect(r.body).toBe(body);
expect(r.meta).toEqual({});
});
it('含 frontmatter 往返', async () => {
const s = new FileService(tmp);
await s.create('', 'doc', false);
const meta = { title: '标题', tags: ['a','b'], star: true, links: ['x/y.md'] };
const body = '# 正文\n内容';
await s.writeDoc({ path: 'doc.md', meta, body });
const r = await s.readDoc('doc.md');
expect(r.meta).toEqual(meta);
expect(r.body).toBe(body);
});
it('重开后内容仍一致(模拟重启)', async () => {
const body = '持久化内容\n- 项一\n- 项二';
await (new FileService(tmp)).create('', 'persist', false);
await (new FileService(tmp)).writeDoc({ path: 'persist.md', meta: { star: true }, body });
// 新实例(模拟重启)读取
const r = await (new FileService(tmp)).readDoc('persist.md');
expect(r.body).toBe(body);
expect(r.meta.star).toBe(true);
});
});
describe('ConfigStore', () => {
it('默认空 meta', async () => {
const cs = new ConfigStore(tmp);
const m = await cs.getVaultMeta();
expect(m.currentRoot).toBe('');
expect(m.recents).toEqual([]);
});
it('setCurrentRoot 记录最近库并去重截断', async () => {
const cs = new ConfigStore(tmp);
await cs.setCurrentRoot('C:/a');
await cs.setCurrentRoot('C:/b');
await cs.setCurrentRoot('C:/a');
const m1 = await cs.getVaultMeta();
expect(m1.currentRoot).toBe('C:/a');
expect(m1.recents).toEqual(['C:/a', 'C:/b']);
// 超出上限截断
for (let i = 0; i < 8; i++) await cs.setCurrentRoot('C:/n' + i);
const m2 = await cs.getVaultMeta();
expect(m2.recents.length).toBeLessThanOrEqual(5);
});
it('重启后持久化(新实例读取)', async () => {
await (new ConfigStore(tmp)).setDefaultRoot('C:/default');
const m = await (new ConfigStore(tmp)).getVaultMeta();
expect(m.defaultRoot).toBe('C:/default');
});
});