update
This commit is contained in:
170
scripts/check-trash-context-menu.mjs
Normal file
170
scripts/check-trash-context-menu.mjs
Normal file
@@ -0,0 +1,170 @@
|
||||
// 回收站右键菜单判定逻辑回归 —— 验证:
|
||||
// [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=true,originalCategory='工作' 保留)→
|
||||
// 任务自动搬到「已完成任务」分类,再删到回收站
|
||||
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');
|
||||
Reference in New Issue
Block a user