This commit is contained in:
2026-09-12 13:55:57 +08:00
commit 30f1cedd39
95 changed files with 42057 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
// 回归:模态/命令面板关闭时的 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);