// 全面 smoke test:验证外部编辑 → 软件反映 import { TaskStore } from '../src/task-store.js'; import { parseMarkdown, CategoryKind } from '../src/markdown-parser.js'; import { writeMarkdown } from '../src/markdown-writer.js'; import { check, ok, bad, summary, printSummary } from './_lib/check.mjs'; import { makeStore } from './_lib/store-fixture.mjs'; import { installApiStub } from './_lib/api-stub.mjs'; // 桩出 window.api,让 TaskStore 不真去写文件 installApiStub(); // ============================================================ // [1] 外部编辑 → 重新加载 → 顺序保留 // ============================================================ console.log('\n[1] 外部编辑调整顺序 → 重新加载 → 顺序保留'); { // 初始:a, b, c const initial = `# 全部任务\n\n## 工作\n\n- [ ] a\n- [ ] b\n- [ ] c\n`; const store = makeStore(initial); const work = store.getCategory('工作'); check('初始顺序 a/b/c', work.tasks.map(t => t.text).join(',') === 'a,b,c', work.tasks.map(t => t.text).join(',')); // 用户在编辑器里改成 c, a, b(保存 → fs.watch → 重新加载) const reordered = `# 全部任务\n\n## 工作\n\n- [ ] c\n- [ ] a\n- [ ] b\n`; store.loadFromContent(reordered, null); const work2 = store.getCategory('工作'); check('重载后顺序变为 c/a/b', work2.tasks.map(t => t.text).join(',') === 'c,a,b', work2.tasks.map(t => t.text).join(',')); // 跨分类移动:在工作里把 x 移到最前 const store2 = makeStore(`# 全部任务\n\n## 工作\n\n- [ ] x\n- [ ] y\n- [ ] z\n\n## 学习\n\n- [ ] k1\n`); const store3 = makeStore(`# 全部任务\n\n## 工作\n\n- [ ] y\n- [ ] z\n- [ ] x\n\n## 学习\n\n- [ ] k1\n`); check('工作分类 y/z/x 顺序保留', store3.getCategory('工作').tasks.map(t => t.text).join(',') === 'y,z,x'); // 顺序往返:parser → writer → parser const md = `# 全部任务\n\n## 工作\n\n- [ ] 第一\n- [ ] 第二\n- [ ] 第三\n`; const cats = parseMarkdown(md); const written = writeMarkdown(cats); const cats2 = parseMarkdown(written); const work3 = cats2.find(c => c.name === '工作'); check('parser→writer→parser 顺序不变', work3.tasks.map(t => t.text).join(',') === '第一,第二,第三'); } // ============================================================ // [2] 外部编辑 → 重新加载 → 文本变更同步 // ============================================================ console.log('\n[2] 外部编辑文本 → 重新加载 → 文本同步'); { const initial = `# 全部任务\n\n## 工作\n\n- [ ] 原文本\n`; const store = makeStore(initial); // 用户改成新文本 const updated = `# 全部任务\n\n## 工作\n\n- [ ] 新文本\n`; store.loadFromContent(updated, null); const work = store.getCategory('工作'); check('文本被新内容覆盖', work.tasks[0].text === '新文本', work.tasks[0].text); check('仍是 1 条任务', work.tasks.length === 1); } // ============================================================ // [3] 外部编辑 → 重新加载 → 删除任务 // ============================================================ console.log('\n[3] 外部编辑删除任务 → 重新加载 → 删除生效'); { const initial = `# 全部任务\n\n## 工作\n\n- [ ] a\n- [ ] b\n- [ ] c\n`; const store = makeStore(initial); const removed = `# 全部任务\n\n## 工作\n\n- [ ] a\n- [ ] c\n`; store.loadFromContent(removed, null); const work = store.getCategory('工作'); check('删除中间那条,剩 a/c', work.tasks.map(t => t.text).join(',') === 'a,c', work.tasks.map(t => t.text).join(',')); } // ============================================================ // [4] 外部编辑 → 重新加载 → 新增任务 // ============================================================ console.log('\n[4] 外部编辑新增任务 → 重新加载 → 新增生效'); { const initial = `# 全部任务\n\n## 工作\n\n- [ ] a\n`; const store = makeStore(initial); const added = `# 全部任务\n\n## 工作\n\n- [ ] a\n- [ ] 新增的\n`; store.loadFromContent(added, null); const work = store.getCategory('工作'); check('新增后 a/新增的', work.tasks.map(t => t.text).join(',') === 'a,新增的', work.tasks.map(t =>t.text).join(',')); } // ============================================================ // [5] 跨分类移动(外部编辑) // ============================================================ console.log('\n[5] 跨分类移动 → 重新加载 → 位置同步'); { const initial = `# 全部任务\n\n## 工作\n\n- [ ] taskA\n\n## 学习\n\n- [ ] taskB\n`; const store = makeStore(initial); const moved = `# 全部任务\n\n## 工作\n\n\n## 学习\n\n- [ ] taskB\n- [ ] taskA\n`; store.loadFromContent(moved, null); const work = store.getCategory('工作'); const study = store.getCategory('学习'); check('taskA 移到学习分类末尾', study.tasks.map(t => t.text).join(',') === 'taskB,taskA'); check('工作分类变空', work.tasks.length === 0); } // ============================================================ // [6] id 运行时分配 + 重新加载重新分配 // ============================================================ console.log('\n[6] 跨重载:task.id 重新分配'); { const initial = `# 全部任务\n\n## 工作\n\n- [ ] a\n- [ ] b\n`; const store1 = makeStore(initial); const work1 = store1.getCategory('工作'); const ids1 = work1.tasks.map(t => t.id); check('初次加载 id 是 t1/t2', ids1.join(',') === 't1,t2', ids1.join(',')); // 重新加载同样内容 store1.loadFromContent(initial, null); const work2 = store1.getCategory('工作'); const ids2 = work2.tasks.map(t => t.id); check('重新加载后 id 也是 t1/t2(确定性)', ids2.join(',') === 't1,t2', ids2.join(',')); check('id 字符串相等(基于位置)', ids1.join(',') === ids2.join(',')); } // ============================================================ // [7] 同名任务在不同分类 → 各自独立 id // ============================================================ console.log('\n[7] 同名任务在不同分类 → 各自独立'); { const md = `# 全部任务\n\n## 工作\n\n- [ ] 买菜\n\n## 生活\n\n- [ ] 买菜\n`; const cats = parseMarkdown(md); const work = cats.find(c => c.name === '工作'); const life = cats.find(c => c.name === '生活'); check('工作和生活都有「买菜」', work.tasks[0].text === '买菜' && life.tasks[0].text === '买菜'); check('两个 id 不同', work.tasks[0].id !== life.tasks[0].id, `工作=${work.tasks[0].id}, 生活=${life.tasks[0].id}`); } // ============================================================ // [8] 同名任务在同一分类 → 多个独立任务 // ============================================================ console.log('\n[8] 同名任务在同一分类 → 多个独立'); { const md = `# 全部任务\n\n## 工作\n\n- [ ] 买菜\n- [ ] 买菜\n`; const cats = parseMarkdown(md); const work = cats.find(c => c.name === '工作'); check('两条「买菜」都在', work.tasks.length === 2); check('两条 id 不同', work.tasks[0].id !== work.tasks[1].id, `${work.tasks[0].id} vs ${work.tasks[1].id}`); } // ============================================================ // [9] 状态标识(⭐/▶/[✓])保留 // ============================================================ console.log('\n[9] 状态标识(⭐/▶/[✓])跨重载保留'); { const initial = `# 全部任务\n\n## 工作\n\n- [ ] [⭐] 重要任务\n- [ ] [▶] 当前任务\n- [✓] 已完成\n`; const store = makeStore(initial); const work = store.getCategory('工作'); check('重要标记', work.tasks[0].important === true); check('当前标记', work.tasks[1].current === true); // v3.4 起:[✓] 任务在加载时自动从源分类搬到「# 已完成任务」真实分类, // 不再停留在原分类里 —— 数据归属明确,源分类里不再保留已完成的「鬼影」。 const completed = store.getCompletedCategory(); check('已完成任务被搬到「# 已完成任务」分类(v3.4)', completed?.tasks?.some(t => t.text === '已完成' && t.completed === true), completed?.tasks?.map(t => `${t.text}(${t.completed})`).join(' | ')); // 重新加载 store.loadFromContent(initial, null); const work2 = store.getCategory('工作'); check('重载后重要标记保留', work2.tasks[0].important === true); check('重载后当前标记保留', work2.tasks[1].current === true); const completed2 = store.getCompletedCategory(); check('重载后已完成任务仍在「# 已完成任务」并 completed=true', completed2?.tasks?.some(t => t.text === '已完成' && t.completed === true), completed2?.tasks?.map(t => `${t.text}(${t.completed})`).join(' | ')); } // ============================================================ // [10] writer 永远不会输出 )→ 加载干净 // ============================================================ console.log('\n[11] 旧文件迁移:含 '); { const old = `# 全部任务\n\n## 工作\n\n- [ ] 旧任务 \n- [ ] 普通任务\n`; const cats = parseMarkdown(old); const work = cats.find(c => c.name === '工作'); check('旧 id 注释被剥掉,文本干净', work.tasks.every(t => !t.text.includes('