update
This commit is contained in:
346
scripts/check-commit-active-edit.mjs
Normal file
346
scripts/check-commit-active-edit.mjs
Normal file
@@ -0,0 +1,346 @@
|
||||
// 回归测试: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] 隐藏的 input(input.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');
|
||||
Reference in New Issue
Block a user