Files
Todo-List/scripts/check-commit-active-edit.mjs
2026-09-12 13:55:57 +08:00

346 lines
14 KiB
JavaScript
Raw Permalink 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.
// 回归测试H12 — render() 顶部 _commitActiveEdit() 同步提交草稿
//
// 背景:旧版 render() 仅在最末尾 _cancelAllDrags没管 _editInput。
// 用户在编辑任务文本时,外部 file:change / 兄弟组件 emit('change') 等触
// 发 render(),会替换 listEl.innerHTML_editInput DOM 节点随之销毁,
// 用户编辑中的草稿凭空消失。修复v3.7+render() 顶部同步调
// _commitActiveEdit(),把 input 当前 value 写到 store。
//
// 本测试在 Node 环境下镜像 _commitActiveEdit 的行为DOM stub
// 验证「编辑态被 render() 触发时草稿是否保留」。
//
// 跑法node scripts/check-commit-active-edit.mjs
import { check, summary, printSummary } from './_lib/check.mjs';
import { makeStore } from './_lib/store-fixture.mjs';
// ── 镜像 _commitActiveEdit 的核心契约 ──
console.log('\n[1] _commitActiveEdit草稿被 render() 触发时同步提交');
{
const store = makeStore('# 全部任务\n\n## 工作\n\n- [ ] 写周报\n\n## 未分类\n\n');
const cat = store.getCategory('工作');
const task = cat.tasks[0];
// 模拟 DOM编辑态 input 节点 + 草稿值
const fakeInput = {
hidden: false,
value: '写周报 + 截止周五',
_listeners: { keydown: null, blur: null },
removeEventListener(type, fn) {
if (this._listeners[type] === fn) this._listeners[type] = null;
}
};
const editState = {
_editInput: fakeInput,
_editTaskId: task.id,
_editRealCatName: '工作',
_editOnKey: () => {}, // 占位remove 时匹配
_editOnBlur: () => {}, // 占位remove 时匹配
};
// 镜像 task-list.js 的 _commitActiveEdit 行为(同步提交到 store
function commitActiveEdit() {
if (!editState._editInput) return;
const input = editState._editInput;
const taskId = editState._editTaskId;
const realCatName = editState._editRealCatName;
if (editState._editOnKey) input.removeEventListener('keydown', editState._editOnKey);
if (editState._editOnBlur) input.removeEventListener('blur', editState._editOnBlur);
editState._editInput = null;
editState._editOnKey = null;
editState._editOnBlur = null;
editState._editTaskId = null;
editState._editRealCatName = null;
if (!input.hidden && taskId && realCatName) {
const newText = input.value.trim();
if (newText) {
store.updateTaskText(realCatName, taskId, newText);
}
}
}
// ── 镜像 render() 顶部逻辑:先 _cancelAllDrags再 _commitActiveEdit ──
function render() {
// render() 模拟:把 DOM 整个重建_editInput 节点会被销毁
// 关键render() 必须先 commit 草稿再销毁
commitActiveEdit();
// 模拟 innerHTML 替换fakeInput 现在应不再被引用
fakeInput.hidden = true;
}
// 触发 render模拟外部 file:change → store.on('change') → task-list.render()
render();
// 断言:草稿已落到 store
const updated = store.getCategory('工作').tasks[0];
check('render() 触发后 store 里的 text 是草稿值(不是空字符串)',
updated.text === '写周报 + 截止周五',
`actual=${JSON.stringify(updated.text)}`);
check('render() 触发后 edit 状态被清空_editInput === null',
editState._editInput === null);
check('render() 触发后 editTaskId 被清空',
editState._editTaskId === null);
}
// ── [2] 空草稿不触发 updateTaskText ──
console.log('\n[2] 空草稿edit value 为空 → 不写入 store保持原值');
{
const store = makeStore('# 全部任务\n\n## 工作\n\n- [ ] 写周报\n\n## 未分类\n\n');
const cat = store.getCategory('工作');
const originalText = cat.tasks[0].text;
const fakeInput = { hidden: false, value: ' ', removeEventListener() {} };
const editState = {
_editInput: fakeInput,
_editTaskId: cat.tasks[0].id,
_editRealCatName: '工作',
_editOnKey: null,
_editOnBlur: null,
};
function commitActiveEdit() {
if (!editState._editInput) return;
const input = editState._editInput;
const taskId = editState._editTaskId;
const realCatName = editState._editRealCatName;
editState._editInput = null;
editState._editTaskId = null;
editState._editRealCatName = null;
if (!input.hidden && taskId && realCatName) {
const newText = input.value.trim();
if (newText) {
store.updateTaskText(realCatName, taskId, newText);
}
}
}
commitActiveEdit();
check('空草稿不写入 store',
store.getCategory('工作').tasks[0].text === originalText,
`actual=${store.getCategory('工作').tasks[0].text}`);
}
// ── [3] hidden input 不触发 commit ──
console.log('\n[3] 隐藏的 inputinput.hidden=true不触发 commit');
{
const store = makeStore('# 全部任务\n\n## 工作\n\n- [ ] 写周报\n\n## 未分类\n\n');
const cat = store.getCategory('工作');
const originalText = cat.tasks[0].text;
const fakeInput = { hidden: true, value: '不应写', removeEventListener() {} };
const editState = {
_editInput: fakeInput,
_editTaskId: cat.tasks[0].id,
_editRealCatName: '工作',
_editOnKey: null,
_editOnBlur: null,
};
function commitActiveEdit() {
if (!editState._editInput) return;
const input = editState._editInput;
const taskId = editState._editTaskId;
const realCatName = editState._editRealCatName;
editState._editInput = null;
editState._editTaskId = null;
editState._editRealCatName = null;
// 注意hidden=true 时跳过写入
if (!input.hidden && taskId && realCatName) {
const newText = input.value.trim();
if (newText) {
store.updateTaskText(realCatName, taskId, newText);
}
}
}
commitActiveEdit();
check('hidden input 不写 store',
store.getCategory('工作').tasks[0].text === originalText);
}
// ── [4] render() 之前 _cancelAllDrags + _commitActiveEdit 顺序正确 ──
console.log('\n[4] render() 顺序:先 commit 编辑、再取消拖拽(按 task-list.js render() 顺序)');
{
// 这里直接验证 task-list.js render() 源码里的调用顺序 —— 防止有人
// 调换顺序导致 commit 时还在拖拽状态。
//
// 注意M1 修复后 _commitActiveEdit() 被 if (this._editInput && this._editInput.isConnected)
// 包了一层(防御 input 已被销毁的场景),源码体积比 600 字符多一点。截取 1200 字符
// 留足缓冲;只要 _commitActiveEdit() 出现在 _cancelAllDrags() 之后即可。
const fs = await import('node:fs');
const src = fs.readFileSync('./src/ui/task-list.js', 'utf8');
const renderStart = src.indexOf('render() {');
const renderSnippet = src.slice(renderStart, renderStart + 1200);
check('render() 顶部调 _cancelAllDrags()',
renderSnippet.includes('_cancelAllDrags()'));
check('render() 顶部调 _commitActiveEdit()(在 _cancelAllDrags 之后)',
renderSnippet.includes('_commitActiveEdit()'));
// 顺序cancel 在 commit 之前
const cancelIdx = renderSnippet.indexOf('_cancelAllDrags()');
const commitIdx = renderSnippet.indexOf('_commitActiveEdit()');
check('顺序_cancelAllDrags 在 _commitActiveEdit 之前',
cancelIdx >= 0 && commitIdx > cancelIdx,
`cancel=${cancelIdx}, commit=${commitIdx}`);
}
// ── [5] 空草稿但 _editDirty=true + originalText 非空 → 不静默丢用户意图 ──
//
// 背景:用户编辑时清空整段文本,外部 store change 触发 render()_commitActiveEdit
// 被同步调用。旧实现value 空 → noop → 任务文本保留 → 用户以为"被清空了"但其实还在。
// 修复commit 时若 wasDirty + originalText 非空 + value 空 → 至少在状态上可观察
// (本测试只验证"任务文本未被静默改写为某种意外值"toast 调用是副作用不在测试范围)。
//
// 此处镜像修复后的 commitActiveEdit含 _editDirty / _editOriginalText 字段)。
{
const store = makeStore('# 全部任务\n\n## 工作\n\n- [ ] 写周报\n\n## 未分类\n\n');
const cat = store.getCategory('工作');
const originalText = cat.tasks[0].text;
const fakeInput = { hidden: false, value: '', _listeners: {}, removeEventListener() {} };
let toastShown = false;
const editState = {
_editInput: fakeInput,
_editTaskId: cat.tasks[0].id,
_editRealCatName: '工作',
_editOnKey: null,
_editOnBlur: null,
_editOnInput: null,
_editDirty: true, // 用户曾触发过 input 事件
_editOriginalText: originalText, // 编辑前原文非空
};
// 镜像修复后的 _commitActiveEdit与 task-list.js 同源公式)
function commitActiveEdit() {
if (!editState._editInput) return;
const input = editState._editInput;
const taskId = editState._editTaskId;
const realCatName = editState._editRealCatName;
const wasDirty = editState._editDirty;
const origText = editState._editOriginalText;
if (editState._editOnKey) input.removeEventListener('keydown', editState._editOnKey);
if (editState._editOnBlur) input.removeEventListener('blur', editState._editOnBlur);
if (editState._editOnInput) input.removeEventListener('input', editState._editOnInput);
editState._editInput = null;
editState._editOnKey = null;
editState._editOnBlur = null;
editState._editOnInput = null;
editState._editTaskId = null;
editState._editRealCatName = null;
editState._editDirty = false;
editState._editOriginalText = null;
if (!input.hidden && taskId && realCatName) {
const newText = input.value.trim();
if (newText) {
store.updateTaskText(realCatName, taskId, newText);
} else if (wasDirty && origText) {
// 修复路径:发 toast测试里用一个 flag 代替 UI 调用)
toastShown = true;
// 不调用 updateTaskText —— 保留原文本
}
}
}
commitActiveEdit();
check('空草稿 + wasDirty + originalText 非空 → store 文本未被改写',
store.getCategory('工作').tasks[0].text === originalText,
`actual=${store.getCategory('工作').tasks[0].text}`);
check('空草稿 + wasDirty + originalText 非空 → toast 标志位被设置(让用户感知到)',
toastShown === true);
check('空草稿 + wasDirty + originalText 非空 → edit 状态已清空',
editState._editInput === null && editState._editDirty === false);
}
// ── [6] 空草稿 + _editDirty=false用户没动过+ originalText 非空 → 走历史 noop 路径 ──
{
const store = makeStore('# 全部任务\n\n## 工作\n\n- [ ] 写周报\n\n## 未分类\n\n');
const cat = store.getCategory('工作');
const originalText = cat.tasks[0].text;
const fakeInput = { hidden: false, value: '', removeEventListener() {} };
let toastShown = false;
const editState = {
_editInput: fakeInput,
_editTaskId: cat.tasks[0].id,
_editRealCatName: '工作',
_editDirty: false,
_editOriginalText: originalText,
};
function commitActiveEdit() {
if (!editState._editInput) return;
const input = editState._editInput;
const taskId = editState._editTaskId;
const realCatName = editState._editRealCatName;
const wasDirty = editState._editDirty;
const origText = editState._editOriginalText;
editState._editInput = null;
editState._editTaskId = null;
editState._editRealCatName = null;
editState._editDirty = false;
editState._editOriginalText = null;
if (!input.hidden && taskId && realCatName) {
const newText = input.value.trim();
if (newText) {
store.updateTaskText(realCatName, taskId, newText);
} else if (wasDirty && origText) {
toastShown = true;
}
}
}
commitActiveEdit();
check('空草稿 + wasDirty=false → 不发 toast用户没动过',
toastShown === false);
check('空草稿 + wasDirty=false → 文本未被改写',
store.getCategory('工作').tasks[0].text === originalText);
}
// ── [7] 原文本就是空(手写 - [x] 这种合法空任务)→ 即使用户清空也不该触发 toast ──
{
const store = makeStore('# 全部任务\n\n## 工作\n\n- [ ] \n\n## 未分类\n\n');
const cat = store.getCategory('工作');
const originalText = cat.tasks[0].text; // ''
const fakeInput = { hidden: false, value: '', removeEventListener() {} };
let toastShown = false;
const editState = {
_editInput: fakeInput,
_editTaskId: cat.tasks[0].id,
_editRealCatName: '工作',
_editDirty: true,
_editOriginalText: originalText, // ''
};
function commitActiveEdit() {
if (!editState._editInput) return;
const input = editState._editInput;
const taskId = editState._editTaskId;
const realCatName = editState._editRealCatName;
const wasDirty = editState._editDirty;
const origText = editState._editOriginalText;
editState._editInput = null;
editState._editTaskId = null;
editState._editRealCatName = null;
editState._editDirty = false;
editState._editOriginalText = null;
if (!input.hidden && taskId && realCatName) {
const newText = input.value.trim();
if (newText) {
store.updateTaskText(realCatName, taskId, newText);
} else if (wasDirty && origText) {
// 关键守卫originalText 必须真存在才发 toast —— 否则手写空任务的场景
// 也会被误报,原文本来就空、用户清空等于啥也没干。
toastShown = true;
}
}
}
commitActiveEdit();
check('空草稿 + wasDirty=true + originalText 为空 → 不发 toast语义上没改变',
toastShown === false);
check('空草稿 + wasDirty=true + originalText 为空 → store 文本保持空字符串',
store.getCategory('工作').tasks[0].text === '');
}
printSummary('check-commit-active-edit');