Files
Todo-List/scripts/check-click-after-drag.mjs
2026-09-12 13:55:57 +08:00

178 lines
7.4 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.
// 回归:拖拽任务刚结束立刻点击 checkbox / ⭐ / ▶ / 编辑 / 删除按钮应该生效,
// 而不是被静默吞掉(间歇性「按钮没反应」现象)。
//
// 历史 bug
// _dragJustEnded 标志位在 pointerup 后被设为 true目的是吞掉浏览器随后派发
// 的合成 click/dblclick避免在松手位置触发意外选择或编辑。但标志位的「清
// 除」只发生在**非 wrapper 区**的 pointerdown 里 —— checkbox / ⭐ / ▶ / 编辑
// / 删除按钮的 pointerdown 都被 early-return 挡在 wrapper 检查之后,标志位
// 永远没机会重置。结果:拖完任务再点这些按钮,点一次没反应(被吞),点第二
// 次pointerdown 重置标志位)才生效。表现为「间歇性按钮无响应」。
//
// 修复:在 pointerdown handler 的最顶端无条件重置 _dragJustEnded同时加一个
// 250ms 兜底定时器(防止任何未来代码路径让标志位永久卡住)。
//
// 测项:
// 1) pointerdown handler 在 wrapper early-return 之前无条件重置 _dragJustEnded
// 2) pointerup handler 在设置 _dragJustEnded = true 后再启一个兜底定时器
// 3) 兜底定时器有 clear 路径destroy + 新一轮 pointerdown
// 4) _setupDrag 初始化时声明 _dragJustEndedTimer 字段
import { readFileSync } from 'node:fs';
import { check, ok, bad, printSummary } from './_lib/check.mjs';
const SRC = readFileSync('src/ui/task-list.js', 'utf8');
let pass = 0, fail = 0;
function expect(name, cond, info) {
if (cond) { ok(name); pass++; } else { bad(name, info); fail++; }
}
// ── 1. 核心修复点 ─────────────────────────────────────────────────────────
// pointerdown handler 最开始(任何 early-return 之前)必须无条件重置 _dragJustEnded。
// 用 [\s\S]{0,1500} 抓取整个 handler body确保检测的是同一个 handler 内部的相对位置。
//
// 回归签名:
// - 必须出现 `this._dragJustEnded = false;` 在 pointerdown handler 内部
// - 该赋值必须早于 `.task-checkbox-wrapper` early-return
// - 不能落在 early-return 之后
// 抓取 pointerdown handler 全部 body
const pointerDownMatch = SRC.match(
/this\.listEl\.addEventListener\(['"]pointerdown['"],\s*\(e\)\s*=>\s*\{([\s\S]*?)\}\);/
);
expect(
'pointerdown handler 可被静态解析',
!!pointerDownMatch,
'没找到 pointerdown handler'
);
if (pointerDownMatch) {
const body = pointerDownMatch[1];
const resetIdx = body.indexOf('this._dragJustEnded = false');
const wrapperEarlyReturnIdx = body.indexOf('.task-checkbox-wrapper');
const buttonEarlyReturnIdx = body.indexOf('.task-action-btn');
expect(
'pointerdown body 包含 `_dragJustEnded = false` 重置',
resetIdx >= 0,
'没找到 _dragJustEnded = false'
);
expect(
'pointerdown body 包含 .task-checkbox-wrapper 早退分支',
wrapperEarlyReturnIdx >= 0,
'没找到 .task-checkbox-wrapper 早退'
);
expect(
'_dragJustEnded 重置位于 wrapper 早退之前(防 wrapper-点击卡死)',
resetIdx >= 0 && wrapperEarlyReturnIdx >= 0 && resetIdx < wrapperEarlyReturnIdx,
`_dragJustEnded 重置在位置 ${resetIdx}wrapper 早退在 ${wrapperEarlyReturnIdx} —— 必须前者在前`
);
// 关键reset 必须在 button early-return 之前(按钮也是 wrapper 的一种)
expect(
'_dragJustEnded 重置位于 .task-action-btn 早退之前(防编辑/删除卡死)',
resetIdx >= 0 && buttonEarlyReturnIdx >= 0 && resetIdx < buttonEarlyReturnIdx,
`重置在 ${resetIdx}action 早退在 ${buttonEarlyReturnIdx}`
);
}
// ── 2. 兜底定时器 ─────────────────────────────────────────────────────────
// pointerup 在设 _dragJustEnded = true 之后必须再启一个 setTimeout 兜底。
// 250ms 上限:浏览器合成 click 一般 < 50ms250ms 已是绝对够用的上限。
//
// 注意pointerup handler 在源码里是命名函数 `const onPointerUp = (e) => { ... }`
// 然后 document.addEventListener('pointerup', onPointerUp) 把它挂上去 ——
// 不是 inline arrow。所以匹配的是 const onPointerUp = ... 整段。
const onPointerUpMatch = SRC.match(
/const\s+onPointerUp\s*=\s*\(e\)\s*=>\s*\{([\s\S]*?)\n\s{4}\};/
);
expect(
'onPointerUp handler 可被静态解析',
!!onPointerUpMatch,
'没找到 onPointerUp 命名函数'
);
if (onPointerUpMatch) {
const body = onPointerUpMatch[1];
const setTrueIdx = body.indexOf('this._dragJustEnded = true');
const timerIdx = body.search(/setTimeout\([\s\S]{0,200}?_dragJustEnded\s*=\s*false/);
expect(
'onPointerUp body 包含 `_dragJustEnded = true`',
setTrueIdx >= 0,
'没找到 _dragJustEnded = true'
);
expect(
'onPointerUp body 包含 250ms 兜底 setTimeout 重置 _dragJustEnded',
timerIdx >= 0,
'没找到 setTimeout(() => { this._dragJustEnded = false }, ...) 兜底'
);
expect(
'兜底 setTimeout 在 _dragJustEnded = true 之后',
setTrueIdx >= 0 && timerIdx >= 0 && timerIdx > setTrueIdx,
`setTimeout 在 ${timerIdx}setTrue 在 ${setTrueIdx}`
);
}
// ── 3. destroy 中清理定时器 ────────────────────────────────────────────────
const destroyMatch = SRC.match(/destroy\(\)\s*\{([\s\S]*?)\n \}/);
expect(
'destroy() 可被静态解析',
!!destroyMatch,
'没找到 destroy() 方法'
);
if (destroyMatch) {
const body = destroyMatch[1];
const clearsTimer = body.includes('_dragJustEndedTimer') &&
body.includes('clearTimeout');
expect(
'destroy() 清理 _dragJustEndedTimer防止组件销毁后定时器还在跑',
clearsTimer,
'destroy() 里没有清理 _dragJustEndedTimer'
);
}
// ── 4. _setupDrag 初始化时声明 _dragJustEndedTimer ─────────────────────────
const setupDragMatch = SRC.match(/_setupDrag\(\)\s*\{([\s\S]*?)\n \}/);
expect(
'_setupDrag() 可被静态解析',
!!setupDragMatch,
'没找到 _setupDrag() 方法'
);
if (setupDragMatch) {
const body = setupDragMatch[1];
// 期望在 _setupDrag 顶部声明 _dragJustEndedTimer = null与 _dragJustEnded = false 同行/相邻)
const declIdx = body.indexOf('this._dragJustEndedTimer');
expect(
'_setupDrag 内声明 _dragJustEndedTimer 字段',
declIdx >= 0,
'没找到 this._dragJustEndedTimer 初始化'
);
}
// ── 5. 选择模式下 ⭐/▶ 真正使用 .task-readonly-indicator 类 ─────────────────
// 防止「CSS 定义了但 JS 永不应用」的死代码状态。
const renderStarMatch = SRC.match(/class="task-star-btn[^"]*"/);
const renderCurMatch = SRC.match(/class="task-current-btn[^"]*"/);
expect(
'⭐ 按钮渲染带条件 class="task-readonly-indicator"',
!!renderStarMatch && /task-readonly-indicator/.test(renderStarMatch[0]),
'class="task-star-btn" 字符串里没有 task-readonly-indicator —— 选择模式下 CSS 仍是死代码'
);
expect(
'▶ 按钮渲染带条件 class="task-readonly-indicator"',
!!renderCurMatch && /task-readonly-indicator/.test(renderCurMatch[0]),
'class="task-current-btn" 字符串里没有 task-readonly-indicator'
);
printSummary('check-click-after-drag', true);