Files
Todo-List/scripts/check-trash-context-menu.mjs
2026-09-12 13:55:57 +08:00

170 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 回收站右键菜单判定逻辑回归 —— 验证:
// [1] 已完成任务在回收站 → 默认项 = 「恢复到「已完成任务」」(行为对齐)
// [2] 未完成任务在回收站 + originalCategory='X' → 默认项 = 「恢复到「X」」
// [3] 未完成任务在回收站 + 无 originalCategory → 默认项 = 「恢复到「未分类」」
// [4] 已完成任务的「恢复到分类…」子菜单被隐藏(死按钮防御)
// [5] 未完成任务的「恢复到分类…」子菜单按候选数 > 1 才出现
// [6] important=true 时 label 是「取消星标」(反向)
// [7] current=true 时 label 是「取消当前」(反向)
// [8] restore 目标里不含保留名分类(回收站/已完成任务/全部任务)
// [边界] 单候选 / 零候选时不显示「恢复到分类…」子菜单
//
// 测试路径:通过 src/ui/task-context-menu.js 的 buildTrashContextMenuItems
// 直接构造 items 数组helper 是 pure function不碰 DOM。helper 同时被
// task-list.js 调,与 UI 共用一份判定逻辑——产品与测试不会发散。
//
// 用法node scripts/check-trash-context-menu.mjs
import { installApiStub } from './_lib/api-stub.mjs';
import { check, summary, printSummary } from './_lib/check.mjs';
import { CategoryKind } from '../src/markdown-parser.js';
// 让 TaskStore 不真去写文件(构造 store 时不会触发,但 toggleTask 等会调 window.api
installApiStub();
const { TaskStore } = await import('../src/task-store.js');
const { buildTrashContextMenuItems } = await import('../src/ui/task-context-menu.js');
// ============================================================
// 构造一个最小文档:工作分类里两条任务(其中一条 ⭐▶),未分类占位
// ============================================================
const seedDoc = [
'# 全部任务', '',
'## 工作', '',
'- [ ] 写周报', '',
'- [ ] 学习项目[⭐][▶]', '',
'## 未分类', ''
].join('\n');
const store = new TaskStore();
store.loadFromContent(seedDoc, null);
store._autoSave = Object.assign(() => {}, { cancel() {}, flush() {} });
const workCat = store.getCategory('工作');
const workActive = workCat.tasks.find(t => t.text === '写周报');
const workImp = workCat.tasks.find(t => t.text === '学习项目');
// 1) 把 workActive 直接删到回收站 —— 未完成 + originalCategory='工作'
store.deleteTask('工作', workActive.id);
const trash = store.getTrashCategory();
const trashActive = trash.tasks.find(t => t.text === '写周报');
// 2) 把 workImp 勾选完成completed=trueoriginalCategory='工作' 保留)→
// 任务自动搬到「已完成任务」分类,再删到回收站
store.toggleTask('工作', workImp.id);
const completedCat = store.categories.find(c => c.kind === 'completed');
const completedTask = completedCat.tasks.find(t => t.text === '学习项目');
store.deleteTask('已完成任务', completedTask.id);
const trashCompleted = trash.tasks.find(t => t.text === '学习项目');
// 候选目标过滤(与 task-list._restoreTargetCandidates(true) 同款语义)
const realTargets = store.categories.filter(c =>
c.kind !== CategoryKind.OTHER_TASKS &&
c.kind !== CategoryKind.TRASH &&
c.kind !== CategoryKind.COMPLETED);
// ============================================================
// [1] 已完成任务在回收站 → 默认项 = 「恢复到「已完成任务」」
// ============================================================
console.log('\n[1] 已完成任务的默认恢复项 label 是「已完成任务」');
const itemsB = buildTrashContextMenuItems(trashCompleted, []);
const firstItem = itemsB[0];
check('已完成任务默认项 label 是「恢复到「已完成任务」」',
firstItem.label === '恢复到「已完成任务」',
`actual: ${JSON.stringify(firstItem)}`);
check('已完成任务默认项 action 仍是 restore-default',
firstItem.action === 'restore-default');
check('已完成任务的 toggle 项 label 是「标记为未完成」',
itemsB.find(i => i.action === 'toggle').label === '标记为未完成');
// ============================================================
// [2] 未完成任务 + originalCategory='工作' → 默认项 = 「恢复到「工作」」
// ============================================================
console.log('\n[2] 未完成任务的默认恢复项跟 originalCategory 走');
const itemsA = buildTrashContextMenuItems(trashActive, []);
check('未完成任务默认项 label 是「恢复到「工作」」',
itemsA[0].label === '恢复到「工作」',
`actual: ${JSON.stringify(itemsA[0])}`);
// ============================================================
// [3] 未完成任务 + 无 originalCategory → 默认项 = 「恢复到「未分类」」
// ============================================================
console.log('\n[3] 无 originalCategory 退到「未分类」');
// 模拟"老格式 / 未登过的任务"场景:构造一个孤儿任务对象,绕过 store 直接喂给 helper
const orphan = { id: 'orphan', text: '老格式任务', completed: false, originalCategory: null };
const itemsC = buildTrashContextMenuItems(orphan, []);
check('无 originalCategory 默认项 label 是「恢复到「未分类」」',
itemsC[0].label === '恢复到「未分类」',
`actual: ${JSON.stringify(itemsC[0])}`);
// ============================================================
// [4] 已完成任务 → 「恢复到分类…」子菜单被隐藏(死按钮防御)
// ============================================================
console.log('\n[4] 已完成任务隐藏「恢复到分类…」子菜单');
const itemsD = buildTrashContextMenuItems(trashCompleted, realTargets);
check('已完成任务菜单不含 restore-header',
!itemsD.some(i => i.action === 'restore-header'));
check('已完成任务菜单不含任何 restore:* 项',
!itemsD.some(i => typeof i.action === 'string' && i.action.startsWith('restore:')));
// ============================================================
// [5] 未完成任务 + 多候选 → 「恢复到分类…」子菜单出现
// ============================================================
console.log('\n[5] 未完成任务多候选时显示「恢复到分类…」');
const itemsE = buildTrashContextMenuItems(trashActive, realTargets);
check('未完成 + 多候选时显示 restore-header',
itemsE.some(i => i.action === 'restore-header'));
const restoreActions = itemsE
.filter(i => typeof i.action === 'string' && i.action.startsWith('restore:'))
.map(i => i.action);
check('未完成 + 多候选时显示 restore:* 目标项',
restoreActions.length > 0,
`actions: ${restoreActions.join(',') || '(无)'}`);
check('restore 目标里不含保留名(回收站/已完成任务/全部任务)',
itemsE.filter(i => i.action?.startsWith('restore:'))
.every(i => i.label !== '回收站' && i.label !== '已完成任务' && i.label !== '全部任务'));
// ============================================================
// [6/7] important=true / current=true 时 label 是「取消星标/取消当前」
// ============================================================
console.log('\n[6/7] important/current 反向 label');
check('completed + important=true 时 important label 是「取消星标」',
itemsD.find(i => i.action === 'important').label === '取消星标');
// v4.1 起 store.toggleTask 勾选时**保留** current 标记([▶] 与 completed 正交),
// 所以 current=true && completed=true 是正常数据形态。这里仍直接构造任务对象
// 验证 helper 的 label 判定(与 [3] 同款,不依赖 store 路径)。
const completedAndCurrent = {
id: 'completed-current',
text: '已勾选且仍标当前',
completed: true,
current: true,
important: false,
originalCategory: '工作'
};
const itemsCurrent = buildTrashContextMenuItems(completedAndCurrent, []);
check('completed + current=true 时 current label 是「取消当前」',
itemsCurrent.find(i => i.action === 'current').label === '取消当前',
`actual: ${JSON.stringify(itemsCurrent.find(i => i.action === 'current'))}`);
// ============================================================
// [边界] 单候选 / 零候选时不显示「恢复到分类…」子菜单
// ============================================================
console.log('\n[边界] 候选数 ≤ 1 不显示子菜单');
const singleTarget = realTargets.slice(0, 1);
const itemsF = buildTrashContextMenuItems(trashActive, singleTarget);
check('未完成 + 单候选时菜单不含 restore-header',
!itemsF.some(i => i.action === 'restore-header'));
const itemsG = buildTrashContextMenuItems(trashActive, []);
check('未完成 + 零候选时菜单不含 restore-header',
!itemsG.some(i => i.action === 'restore-header'));
printSummary('check-trash-context-menu');