22/**
33 * @ai -context ConfirmDialog.test.tsx —— 危险确认的**语义契约**测试(批 0-D Task 8;规格 §5.3)。
44 *
5- * Why jsdom:弹层经 `Modal` 走 portal 且带焦点陷阱 ⇒ 要真容器与 `document.activeElement`。
6- * 本文件钉住四件事:① §5.3 的硬要求「框内必须列明级联影响与保留项」;② **危险语义用色**
7- * (控制方 2026-09-11 裁决③ · §4.1「绝不用于按钮」)—— 确认按钮保持中性,`--ed-stamp` 绝不出现在
8- * 任何 `background*` 声明里(与 Task 14 Step 1 第 4 条的批次守卫**同向**);③ **复用 Modal 内核**:
9- * 直接扫 `ConfirmDialog.tsx` 的源码作机器证据(含 `from "./Modal"`、不含 Portal 与层级标尺的符号),
10- * 并断言本原语 CSS 无层级、无定位;④ 「可中断 / 可反向 / 重复触发」的确定行为(§8.6.1 第 3 条)。
5+ * Why jsdom:弹层经 `Modal` 走 portal 且带焦点陷阱 ⇒ 要真容器与 `document.activeElement`(同理,
6+ * 样式判据只能读文本:`vitest.config.ts` 的 `css` 为 false ⇒ 测试环境不加载样式表,范式 C)。
7+ * 本文件钉住五件事:① §5.3「框内必须列明级联影响与保留项」;② **危险语义用色**:确认按钮保持中性、
8+ * `--ed-stamp` 绝不出现在任何 `background*` 声明里(裁决③ · 与 Task 14 第 4 条守卫**同向**);③ **复用
9+ * Modal 内核**(扫源码作证据:含 `from "./Modal"`、不含 Portal 与层级标尺符号,且本原语 CSS 无层级无定位);
10+ * ④ 可中断 / 可反向 / 重复触发」的确定行为(§8.6.1 第 3 条);⑤ 退场相位(`open=false` 仍在淡出的 160ms)
11+ * 内整个对话框失活 —— 点击即时、退场异步 ⇒ 第二次点击会落到按钮上(评审 I-1,见该 describe 的用例)。
1112 *
12- * 样式判据读文本:`vitest.config.ts` 的 `css` 为 false(测试环境不加载样式表)⇒ 范式 C(同 Modal.test.tsx)。
1313 * 副作用:无(只挂 React 树 + 读三个文本文件);假计时器只用在推进 presence 计时的那个 describe。
1414 * 边界:不用 jest-dom(本仓未装)—— 一律 `getAttribute` / `textContent` / `document.activeElement`。
1515 */
@@ -36,10 +36,9 @@ const IMPACTS: readonly ConfirmImpact[] = [
3636 { text : "笔记 7 篇保留" , keep : true } ,
3737] ;
3838
39- /** 宿主 props:派生自原语契约(每个用例只关心其中几项 ⇒ `Partial` + 必需的 `open`) */
39+ /** 宿主 props:派生自原语契约(`Partial` + 必需的 `open`);受控形态,`open` 由测试 rerender 驱动 */
4040type HostProps = Partial < ConfirmDialogProps > & { open : boolean } ;
4141
42- /** 宿主:受控形态(与真实调用点一致,`open` 由测试 rerender 驱动) */
4342function Host ( { open, title = "删除「高数」?" , onConfirm = noop , onCancel = noop , ...rest } : HostProps ) {
4443 return < ConfirmDialog open = { open } title = { title } onConfirm = { onConfirm } onCancel = { onCancel } testId = "cd" { ...rest } /> ;
4544}
@@ -271,4 +270,28 @@ describe("⑦ 进出场时机(presence 由 Modal 承载)", () => {
271270 expect ( onCancel ) . not . toHaveBeenCalled ( ) ;
272271 expect ( onConfirm ) . not . toHaveBeenCalled ( ) ;
273272 } ) ;
273+
274+ it ( "反例守门:退场相位(open=false 且仍挂载)内点确认 / 取消 / 遮罩 **一个回调都不产生**" , ( ) => {
275+ const onConfirm = vi . fn ( ) ;
276+ const onCancel = vi . fn ( ) ;
277+ const { rerender } = render ( < Host open onConfirm = { onConfirm } onCancel = { onCancel } /> ) ;
278+ tick ( 0 ) ;
279+ rerender ( < Host open = { false } onConfirm = { onConfirm } onCancel = { onCancel } /> ) ;
280+ expect ( panel ( ) . getAttribute ( "data-phase" ) ) . toBe ( "exit" ) ;
281+ fireEvent . click ( confirmBtn ( ) ) ; // 批 4 的误触面:点了删除、弹层在淡出、手一抖又点到确认
282+ fireEvent . click ( cancelBtn ( ) ) ;
283+ fireEvent . mouseDown ( screen . getByTestId ( "cd-overlay" ) ) ;
284+ expect ( [ onConfirm . mock . calls . length , onCancel . mock . calls . length ] , "退场期不得再接受任何交互" ) . toEqual ( [ 0 , 0 ] ) ;
285+ } ) ;
286+
287+ it ( "对照组:**打开态**下三条路径各仍触发一次(防「把功能一起关掉」)" , ( ) => {
288+ const onConfirm = vi . fn ( ) ;
289+ const onCancel = vi . fn ( ) ;
290+ render ( < Host open onConfirm = { onConfirm } onCancel = { onCancel } /> ) ;
291+ fireEvent . click ( confirmBtn ( ) ) ;
292+ fireEvent . click ( cancelBtn ( ) ) ;
293+ fireEvent . mouseDown ( screen . getByTestId ( "cd-overlay" ) ) ;
294+ // confirm 1 次;cancel 2 次 = 取消钮 1 + 遮罩 1
295+ expect ( [ onConfirm . mock . calls . length , onCancel . mock . calls . length ] , "confirm/cancel 次数" ) . toEqual ( [ 1 , 2 ] ) ;
296+ } ) ;
274297} ) ;
0 commit comments