Files
Todo-List/scripts/check-modal-close-animation.mjs
2026-09-12 13:55:57 +08:00

166 lines
7.3 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.
// 回归:模态/命令面板关闭时的 250ms 兜底必须 resolve Promise。
//
// 旧实现animationend 漏触发(如 display:none / 元素被提前 detach
// Promise 永远挂住 → 对 confirmDialog 是 await 卡死、对 command-palette
// 是 _active 永不清 → Ctrl+K 永久失效(致命)。
//
// 验证项:
// (a) 所有 close 站点confirmDialog / inputDialog / openCommandPalette /
// openSettingsDialog 的 finish / cancel / openConflictDialog /
// openDiffPreview / openImportDialog都调用统一的 finalize 帮手
// (b) finalize 里 focus() 必须在 overlay.remove() 之前 —— 否则焦点跳 body
// (c) 250ms setTimeout 必须以 finalize 作为回调(而不是只 remove 不 resolve
// (d) settings-dialog 的 finish / cancel 必须有共享 closed 守卫防双 resolve
import { check, ok, bad, summary, printSummary } from './_lib/check.mjs';
import { readFileSync } from 'node:fs';
const FEEDBACK = readFileSync('src/ui/feedback.js', 'utf8');
const SETTINGS_DIALOG = readFileSync('src/ui/settings-dialog.js', 'utf8');
const COMMAND_PALETTE = readFileSync('src/ui/command-palette.js', 'utf8');
const CONFLICT_DIALOG = readFileSync('src/ui/conflict-dialog.js', 'utf8');
const DIFF_PREVIEW = readFileSync('src/ui/diff-preview-dialog.js', 'utf8');
const IMPORT_DIALOG = readFileSync('src/ui/import-dialog.js', 'utf8');
let pass = 0, fail = 0;
function expect(name, cond, info) {
if (cond) { ok(name); pass++; } else { bad(name, info); fail++; }
}
// 通用 helper验证 close 函数片段是否使用 finalize / 共享 setTimeout
function expectCloseFinalize(label, source, closeNeedle) {
const startIdx = source.indexOf(closeNeedle);
if (startIdx < 0) {
bad(`${label}: 找不到 close 锚点 ${closeNeedle}`);
fail++;
return;
}
// 取 close 函数片段到下一个 const XXX / function / export 之前
const tail = source.slice(startIdx);
const endMatch = tail.match(/\n (?:const |function |export |\})/);
const endIdx = endMatch ? startIdx + endMatch.index : source.length;
const closeBlock = source.slice(startIdx, endIdx);
expect(
`${label} 使用 finalize() 帮手`,
/const finalize\s*=/.test(closeBlock),
`${label} 里没找到 const finalize`
);
expect(
`${label} 的 250ms 兜底调用 finalize不是只 remove`,
/setTimeout\(finalize,\s*250\)/.test(closeBlock),
`${label} 的 250ms 兜底不是 finalize —— animationend 漏触发时 Promise 挂住`
);
expect(
`${label} 中 focus 在 remove 之前`,
/finalize[\s\S]*?previouslyFocused\.focus\(\)[\s\S]*?overlay\.remove\(\)/.test(closeBlock),
`${label}.finalize 里 focus() 顺序在 remove() 之后 —— 被销毁祖先上 focus 跳 body`
);
}
// (a) confirmDialog close使用 finalize / setTimeout(finalize, 250) 模式
{
// 抓 confirmDialog 整段代码 —— 从 'export function confirmDialog' 到 'export function inputDialog'
const startIdx = FEEDBACK.indexOf('export function confirmDialog');
const endIdx = FEEDBACK.indexOf('export function inputDialog');
const confirmBlock = FEEDBACK.slice(startIdx, endIdx);
expect(
'confirmDialog.close 使用 finalize() 帮手',
/const finalize\s*=/.test(confirmBlock),
'confirmDialog 里没找到 const finalize'
);
expect(
'confirmDialog.close 的 250ms 兜底调用 finalize不是只 remove',
/setTimeout\(finalize,\s*250\)/.test(confirmBlock),
'confirmDialog 的 250ms 兜底不是 finalize —— animationend 漏触发时 Promise 挂住'
);
expect(
'confirmDialog.close 中 focus 在 remove 之前',
/finalize[\s\S]*?previouslyFocused\.focus\(\)[\s\S]*?overlay\.remove\(\)/.test(confirmBlock),
'confirmDialog.finalize 里 focus() 顺序在 remove() 之后 —— 被销毁祖先上 focus 跳 body'
);
}
// (b) inputDialog close同样模式
{
const startIdx = FEEDBACK.indexOf('export function inputDialog');
// inputDialog 是最后一个 export下一个 export 是 inputDialog 内部函数 inputDialog
// 取下一个 export function 之前的所有内容(如果没有下一个,则取到文件末尾)
const endIdx = FEEDBACK.indexOf('export function inputDialog', startIdx + 1);
const inputBlock = endIdx > 0 ? FEEDBACK.slice(startIdx, endIdx) : FEEDBACK.slice(startIdx);
expect(
'inputDialog.close 使用 finalize() 帮手',
/const finalize\s*=/.test(inputBlock),
'inputDialog 里没找到 const finalize'
);
expect(
'inputDialog.close 的 250ms 兜底调用 finalize',
/setTimeout\(finalize,\s*250\)/.test(inputBlock),
'inputDialog 的 250ms 兜底不是 finalize'
);
expect(
'inputDialog.close 中 focus 在 remove 之前',
/finalize[\s\S]*?previouslyFocused\.focus\(\)[\s\S]*?overlay\.remove\(\)/.test(inputBlock),
'inputDialog.finalize 里 focus() 顺序在 remove() 之后'
);
}
// (c) command-palette close同样模式
{
const startIdx = COMMAND_PALETTE.indexOf('const close = () =>');
const endIdx = COMMAND_PALETTE.indexOf(' const execute', startIdx);
const closeBlock = COMMAND_PALETTE.slice(startIdx, endIdx > 0 ? endIdx : COMMAND_PALETTE.length);
expect(
'command-palette.close 使用 finalize() 帮手',
/const finalize\s*=/.test(closeBlock),
'command-palette.close 里没找到 const finalize'
);
expect(
'command-palette.close 的 250ms 兜底调用 finalize防 _active 永不清)',
/setTimeout\(finalize,\s*250\)/.test(closeBlock),
'command-palette 的 250ms 兜底不是 finalize —— 这是致命:会让 Ctrl+K 永久失效'
);
}
// (e) conflict-dialog close避免 Promise 永久挂起让 _doResolveExternalChangeConflict 后续
// saveNow / toast 永远不执行
expectCloseFinalize('conflict-dialog.close', CONFLICT_DIALOG, 'const close = (result) =>');
// (f) diff-preview-dialog close用户关掉行级 diff 后焦点必须回到 conflict-dialog「查看完整 diff」按钮
expectCloseFinalize('diff-preview.close', DIFF_PREVIEW, 'const close = () =>');
// (g) import-dialog close避免 _addFromImport 后续逻辑挂起
expectCloseFinalize('import-dialog.close', IMPORT_DIALOG, 'const close = (result) =>');
// (d) settings-dialog 的 finish / cancel 都用 finalize + 共享 closed 守卫
expect(
"settings-dialog 有共享 'closed' 守卫(防 finish/cancel 双竞态)",
/let closed = false/.test(SETTINGS_DIALOG) &&
/const cancel[\s\S]*?if \(closed\) return/.test(SETTINGS_DIALOG) &&
/const finish[\s\S]*?if \(closed\) return/.test(SETTINGS_DIALOG),
"settings-dialog 缺 'closed' 守卫 —— finish/cancel 可并发执行,丢设置"
);
expect(
'settings-dialog.finish 用 finalize()',
/const finish[\s\S]*?const finalize\s*=/.test(SETTINGS_DIALOG),
'settings-dialog.finish 里没找到 finalize'
);
expect(
'settings-dialog.cancel 用 finalize()',
/const cancel[\s\S]*?const finalize\s*=/.test(SETTINGS_DIALOG),
'settings-dialog.cancel 里没找到 finalize'
);
expect(
'settings-dialog.finish 的 250ms 兜底调用 finalize',
/setTimeout\(finalize,\s*250\)/.test(SETTINGS_DIALOG),
'settings-dialog.finish 的 250ms 兜底不是 finalize'
);
expect(
'settings-dialog.cancel 的 250ms 兜底调用 finalize',
SETTINGS_DIALOG.match(/const cancel[\s\S]*?setTimeout\(finalize, 250\)/) !== null,
'settings-dialog.cancel 的 250ms 兜底不是 finalize'
);
printSummary(pass, fail);