// tests/unit/renderer/components/standalone-editor-id-drift.test.tsx // // 守 r192「保存时 id 漂移」在**编辑器**这一侧的行为契约。 // // 场景(用户可复现): // 1. 用户打开一篇笔记,改了标题(标记 dirty); // 2. 自动保存触发 —— 主进程把 frontmatter id 重钉为路径哈希,返回**新 id**; // 3. 用户**在保存往返期间**又敲了几个字(新内容还没落盘); // 4. renderer 拿到新 id,编辑器收到的 note prop 的 id 变了。 // // 旧行为(两处都丢字): // - 编辑器把「id 变了」当成**切到另一篇笔记**:用**已死的旧 id** 去 flush // (主进程已删掉旧 byId 键)⇒ 必撞 NOT_FOUND ⇒ 正文只进 failedDrafts // (旧 id 键)⇒ 再也读不回来; // - flush 之后 setBody(note.body) / setTitle(note.title) 把用户在保存之后 // 新敲的字**直接擦掉**(note.body 是保存那一刻的快照,不含第 3 步的字)。 // // 本测锁(行为级,只锁用户可见的行为,不锁实现): // 1. 同一个 relPath、id 变了 ⇒ **不得**再用旧 id 发起保存; // 2. 本地正文 / 标题**不被**回滚(用户保存后敲的字还在); // 3. 反面对照:换成**另一篇**笔记(relPath 不同)⇒ 仍然照旧 flush 旧笔记 // (不能把「切笔记」这条路一起关掉)。 import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { render, fireEvent, screen, act, cleanup } from '@testing-library/react'; // i18n:只关心交互,不关心文案 —— key 原样返回 vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (k: string) => k }), })); // workspace store:编辑器只用到 pendingSaveActiveNote.version(⌘S 通道) // + consumeSaveActiveNoteRequest();本测不走 ⌘S,后者恒 false。 vi.mock('../../../../src/renderer/src/store/workspace', () => { const state = { pendingSaveActiveNote: { version: 0 }, consumeSaveActiveNoteRequest: () => false, }; const useWorkspaceStore = ((selector?: (s: unknown) => unknown) => selector ? selector(state) : state) as unknown as { (selector?: (s: unknown) => unknown): unknown; getState: () => unknown; }; useWorkspaceStore.getState = () => state; return { useWorkspaceStore }; }); // notes store:编辑器只在两处读它 —— // 1) 切笔记 flush 前查「旧笔记还在不在列表里」(mustContain 旧 id,否则 // 走 skip 分支,测不到死 id flush); // 2) ⌘S 通道读 selectedId(本测不走)。 vi.mock('../../../../src/renderer/src/store/notes', () => { const state = { notes: [{ id: 'oldid', relPath: 'a.md' }], selectedId: 'oldid', }; return { useNotesStore: { getState: () => state, }, }; }); // MarkdownEditor 是 CodeMirror(jsdom 里不可测)⇒ 换成普通 textarea, // 行为等价(受控 value + onChange(string))。 vi.mock('../../../../src/renderer/src/components/editor/MarkdownEditor', () => ({ MarkdownEditor: (props: { value: string; onChange: (v: string) => void }) => (