Files
Literature-Management/tests/unit/main/literature-folder-membership.test.ts
2026-10-21 14:42:59 +08:00

105 lines
4.4 KiB
TypeScript

// tests/unit/main/literature-folder-membership.test.ts
//
// 守 literature 域的「文件夹归属」(r172)。
//
// 用户报「粘贴功能有问题」的文献侧第三层根因:文献行**没有 relPath**。
// renderer 的 visibleItemRows 按 dirname(relPath) 分组(根 = ''),没有 relPath
// ⇒ dirname 恒为 '' ⇒ 文献行永远只在根视图可见,子目录视图恒空。
// 于是「复制/粘贴进文件夹」「OS 拖进文件夹」文件确实搬到子目录了,列表却
// 还显示在根 —— 用户视角就是「粘贴没生效」。
//
// 旧代码的注释写着「Phase 1 临时把 absPath 当 relPath 走」,但**只做了
// type cast,没有任何映射** —— 注释是假的,行为是坏的。本用例锁住真实行为:
// 1. 导入到根 → relPath = 文件名
// 2. 搬进子目录 → relPath = 'sub/x.pdf'(且 id 随 absPath 变)
// 3. 直接丢在磁盘子目录里的文件(不经 UI)→ reload 后也能被发现并带上
// 正确的 relPath(递归扫描,旧实现只看顶层)
// 4. 越界文件(不在 literature 子树内)→ relPath = '' 归到根视图
//
// 真库(electron app.getPath)在测试环境不可用 → resources-repo 整体 mock;
// 被测行为(relPath 计算 + 递归扫描 + byId 维护)全在 service 内。
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { promises as fs } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
const repo = vi.hoisted(() => ({
upsertResource: vi.fn(),
deleteResource: vi.fn(),
// r196:文献 id 换行走**单事务** renameResource(upsert 新行 + 删旧行),
// 不再是 delete + upsert 两个独立事务 —— 见 literature-rename.test.ts。
renameResource: vi.fn(),
listResources: vi.fn(() => [] as unknown[]),
}));
vi.mock('../../../src/main/db/resources-repo', () => ({
upsertResource: repo.upsertResource,
deleteResource: repo.deleteResource,
renameResource: repo.renameResource,
listResources: repo.listResources,
}));
import { setRootPath } from '../../../src/main/utils/safe-write';
import { LiteratureService, litRelPathOf } from '../../../src/main/literature/literature-service';
describe('LiteratureService 文件夹归属(relPath,r172)', () => {
let root: string;
let litDir: string;
let svc: LiteratureService;
beforeEach(async () => {
root = await fs.mkdtemp(join(tmpdir(), 'rkms-lit-rel-'));
setRootPath(root);
litDir = join(root, 'literature');
await fs.mkdir(litDir, { recursive: true });
svc = new LiteratureService();
await svc.start({ skipInitialLoad: true });
});
afterEach(async () => {
await svc.stop();
await fs.rm(root, { recursive: true, force: true });
});
it('导入到根 → relPath = 文件名(根视图)', async () => {
const src = join(root, 'paper.pdf');
await fs.writeFile(src, 'PDF-BYTES', 'utf-8');
const imported = await svc.importFile(src);
const row = svc.findById(imported.id);
expect(row?.relPath).toBe('paper.pdf');
});
it('搬进子目录 → relPath 变成 sub/x.pdf(旧实现没有 relPath,列表永远在根)', async () => {
const src = join(root, 'paper.pdf');
await fs.writeFile(src, 'PDF-BYTES', 'utf-8');
const imported = await svc.importFile(src);
const moved = await svc.moveFile(imported.id, 'sub');
const row = svc.findById(moved.id);
expect(row?.relPath).toBe('sub/paper.pdf');
expect(moved.id).not.toBe(imported.id);
});
it('直接丢在磁盘子目录里的文件 → reload 后被发现并带正确 relPath', async () => {
await fs.mkdir(join(litDir, 'sub', 'deep'), { recursive: true });
await fs.writeFile(join(litDir, 'sub', 'deep', 'manual.pdf'), 'PDF', 'utf-8');
await svc.load();
const rows = svc.list();
expect(rows).toHaveLength(1);
expect(rows[0]?.relPath).toBe('sub/deep/manual.pdf');
});
it('reload 仍能扫到根目录的文件(递归改造没把顶层漏掉)', async () => {
await fs.writeFile(join(litDir, 'top.pdf'), 'PDF', 'utf-8');
await svc.load();
const rows = svc.list();
expect(rows).toHaveLength(1);
expect(rows[0]?.relPath).toBe('top.pdf');
});
it('litRelPathOf:越界路径(不在 literature 子树内)→ 空串归根', () => {
expect(litRelPathOf(litDir, join(litDir, 'sub', 'x.pdf'))).toBe('sub/x.pdf');
expect(litRelPathOf(litDir, join(root, 'elsewhere', 'x.pdf'))).toBe('');
expect(litRelPathOf(litDir, litDir)).toBe('');
});
});