Files
Literature-Management/tests/unit/renderer/store/failed-drafts.test.ts
2026-10-21 14:42:59 +08:00

95 lines
3.8 KiB
TypeScript

// tests/unit/renderer/store/failed-drafts.test.ts
//
// 守卫(r206,2026-10-20):笔记保存失败时的草稿缓冲(store/failed-drafts.ts)。
//
// 为什么值得锁:这个 Map 是**用户丢字前的最后一道保险** —— 保存失败
// (CONFLICT / IPC 断 / 磁盘满)时把 body/title 快照进来,用户重开面板时
// 还能捞回来。静默丢字是这类应用最不可接受的故障,且**不会报错**
// (失败路径本来就静默)。
//
// 两条硬不变式:
// ① **模块级单例**(不是 ref):ref 随组件 unmount 消失,而「关面板 →
// 重开」正是最需要草稿活着的场景;组件级持有还会形成
// store/notes → components/StandaloneNoteEditorCore → store/notes 的
// 循环依赖(r35 就是为这个把它抽到零依赖叶子模块的)。
// ② **零 import**:本文件不许 import 任何东西 —— 一旦 import 了
// store/ 之类的东西,循环依赖就可能重新长回来(而 layering 守卫只管
// 进程边界,管不到 renderer 内部)。这是 r206 加的**结构守卫**。
import { readFileSync } from 'fs';
import { join } from 'path';
import { beforeEach, describe, expect, it } from 'vitest';
import {
clearFailedDraft,
getFailedDraft,
putFailedDraft,
} from '@renderer/store/failed-drafts';
const SRC = join(process.cwd(), 'src/renderer/src/store/failed-drafts.ts');
describe('failed-drafts(r206)', () => {
beforeEach(() => {
// 清理上一例残留(模块级 Map 跨用例存活)
for (const id of ['a', 'b', 'c']) clearFailedDraft(id);
});
it('put 之后 get 得到同一个草稿', () => {
putFailedDraft('a', { body: 'body-1', title: 'title-1' });
expect(getFailedDraft('a')).toEqual({ body: 'body-1', title: 'title-1' });
});
it('get 未暂存的 id → undefined', () => {
expect(getFailedDraft('never-put')).toBeUndefined();
});
it('同一 id 重复 put 覆盖旧值(不追加、不排队)', () => {
putFailedDraft('a', { body: 'old', title: 'old' });
putFailedDraft('a', { body: 'new', title: 'new' });
expect(getFailedDraft('a')).toEqual({ body: 'new', title: 'new' });
});
it('不同 id 各自独立', () => {
putFailedDraft('a', { body: 'A', title: 'A' });
putFailedDraft('b', { body: 'B', title: 'B' });
expect(getFailedDraft('a')?.body).toBe('A');
expect(getFailedDraft('b')?.body).toBe('B');
});
it('clear 之后 get → undefined', () => {
putFailedDraft('a', { body: 'x', title: 'y' });
clearFailedDraft('a');
expect(getFailedDraft('a')).toBeUndefined();
});
it('clear 未暂存的 id 不抛(幂等)', () => {
expect(() => clearFailedDraft('never-put')).not.toThrow();
});
it('空字符串 body / title 也照常存取(不因 falsy 被吞)', () => {
putFailedDraft('a', { body: '', title: '' });
expect(getFailedDraft('a')).toEqual({ body: '', title: '' });
});
it('草稿是**同一个对象引用**(不被悄悄拷贝/规范化)', () => {
const draft = { body: 'B', title: 'T' };
putFailedDraft('a', draft);
expect(getFailedDraft('a')).toBe(draft);
});
it('模块级单例:重复 import 拿到同一份 Map', async () => {
const again = await import('@renderer/store/failed-drafts');
putFailedDraft('c', { body: 'C', title: 'C' });
expect(again.getFailedDraft('c')).toEqual({ body: 'C', title: 'C' });
});
it('结构不变式:本文件零 import(不许长回循环依赖)', () => {
const src = readFileSync(SRC, 'utf8');
// 去掉注释行再找 import —— 注释里提到 import 是允许的
const code = src
.split('\n')
.filter((l) => !l.trimStart().startsWith('//'))
.join('\n');
const imports = code.match(/^\s*import\s.*$/gm) ?? [];
expect(imports, `failed-drafts.ts 不许有 import(现状):\n${imports.join('\n')}`).toEqual([]);
});
});