Files
gplan-2/game/tests/unit/presentation/modal_flow_test.gd
T
DKChan 8b9e55673f refactor(迭代27 T4): 波四 ModalFlow 抽取+互斥域 tags 化+双 context——对拍全同
P1-5/P2-7 组合根可测化+模态协议内聚(场景拆分后重估结论=抽取面收敛为模态编排域):
- modal_flow.gd 新纯逻辑类: 依赖仅 ModalStack+查询 Callable(审计处方)——开合编排
  (挂起兜底/排播排空/遮挡记账/开合同步)/棋盘输入锁谓词(三例外注入)/双形制角标谓词;
  V6 modal_flow_test 19 断言直接 new() 进 run_all(26→27 套)——测试抓出并修正
  挂起兜底 divergent return 一处(原 main 语义=先结挂起后继续开本层不短路)
- ModalStack 互斥域随层登记: register(m, tags) + TAG_NON_LOCKING/TAG_SCREEN_STATE
  封闭词表 + has_open_excluding_tags 组查询;原 has_open_excluding/_any 调用点拼装退役
  (main 五处判据迁移);main._modals=flow.stack(ui_struct _list 登记序断言缝保留)
- P1-5 控制器构造收拢 context: curio 16 参→CurioFlowContext/camp 13 参→CampFlowContext
  (字段逐一同名迁移,控制器可独立实例化)

验收: 对拍三变体全同;冒烟 1079/0+run_all 27 套+probe click+render golden 全绿
2026-10-15 00:31:36 +08:00

114 lines
6.2 KiB
GDScript
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.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 模态编排纯逻辑类单测(迭代 27 T4——需求卡片 V6 验收条目:ModalFlow 直接 new() 进 run_all;
# 互斥域谓词/三例外输入锁/演播后排窗挂起/演播完边沿派发/遮挡记账——原组合根注释纪律面的
# 结构化单测缝首次落地)。载体零场景零树——纯 RefCounted + 哑 Control。
# 运行:godot --headless --path game --script res://tests/unit/presentation/modal_flow_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ModalFlowScript := preload("res://presentation/modal_flow.gd") # 探针载体禁全局类名裸引用惯例同源
const ModalStackScript := preload("res://presentation/modal_stack.gd")
func _init() -> void:
# 哑层与桩 Callable(纯逻辑类零场景前提的证明面)
var start := Control.new()
var inter := Control.new()
var settlement_holder := {"s": null} # lambda 对局部按值捕获——容器装载
var rows := {"empty": true}
var calls := {"flush": 0, "open_sync": 0, "close_sync": 0, "replay": 0}
var flow = ModalFlowScript.new()
Kit.check_quiet(flow != null and flow.stack != null, "V6 ModalFlow 直接 new()(stack 内建)〔迭代27 T4〕")
flow.start_visible_get = func() -> bool: return start.visible
flow.inter_visible_get = func() -> bool: return inter.visible
flow.settlement_get = func() -> Variant: return settlement_holder["s"]
flow.rows_empty_get = func() -> bool: return rows["empty"]
flow.flush_rows = func() -> void: calls["flush"] += 1
flow.on_open_sync = func() -> void: calls["open_sync"] += 1
flow.on_close_sync = func() -> void: calls["close_sync"] += 1
flow.replay_click = func() -> void: calls["replay"] += 1
# V6-1 互斥域登记+遮挡记账
var a := Control.new() # 普通层
var dbg := Control.new() # 调试浮窗(non_locking)
var map := Control.new() # 关卡地图屏(screen_state)
flow.register(a)
flow.register(dbg, [ModalStackScript.TAG_NON_LOCKING])
flow.register(map, [ModalStackScript.TAG_SCREEN_STATE])
var tag_dump := "tags a=%s dbg=%s map=%s const=%s" % [str(flow.stack._tags.get(a, "MISSING")), str(flow.stack._tags.get(dbg, "MISSING")), str(flow.stack._tags.get(map, "MISSING")), ModalStackScript.TAG_NON_LOCKING]
print("[DBG] ", tag_dump)
flow.open(a)
Kit.check_quiet(a.visible and calls["open_sync"] == 1 and calls["flush"] == 1, "V6 开侧编排(flush→open→sync 单口序)〔迭代27 T4〕")
# 遮挡记账(登记序=z 序:开低层隐其上方可见层、关闭恢复——低层开启期高层照会不可见的呈现保真)
flow.open(map)
Kit.check_quiet(map.visible, "V6 开最高层(上方无可隐)〔迭代27 T4〕")
flow.open(a)
Kit.check_quiet(a.visible and not map.visible, "V6 遮挡记账(开低层隐其上方可见层)〔迭代27 T4〕")
flow.close(a)
Kit.check_quiet(map.visible and calls["close_sync"] == 1, "V6 关闭恢复(关低层显其上方被隐层+关侧同步)〔迭代27 T4〕")
# V6-2 输入锁谓词:互斥域随层登记(non_locking 不计)+ 三例外
start.visible = false
inter.visible = false
settlement_holder["s"] = Object.new()
flow.open(dbg)
Kit.check_quiet(not flow.locks_input(), "V6 输入锁:调试浮窗 non_locking 不计(T2 防线)〔迭代27 T4〕")
flow.close(dbg)
flow.open(a)
Kit.check_quiet(flow.locks_input(), "V6 输入锁:普通层开态计锁〔迭代27 T4〕")
flow.close(a)
flow.close(map) # 登记册全关(close(a) 经遮挡恢复 map——须一并关账才是「全关」态)
settlement_holder["s"] = null
Kit.check_quiet(flow.locks_input(), "V6 输入锁例外三:战斗缺位即锁〔迭代27 T4〕")
settlement_holder["s"] = Object.new()
Kit.check_quiet(not flow.locks_input(), "V6 输入锁:战斗在座+登记册全关=不锁〔迭代27 T4〕")
start.visible = true
Kit.check_quiet(flow.locks_input(), "V6 输入锁例外一:开始层可见即锁〔迭代27 T4〕")
start.visible = false
inter.visible = true
Kit.check_quiet(flow.locks_input(), "V6 输入锁例外二:过场层可见即锁〔迭代27 T4〕")
inter.visible = false
Kit.check_quiet(not flow.locks_input(), "V6 输入锁:全例外解除+登记册全关=不锁〔迭代27 T4〕")
# V6-3 角标谓词(状态/调试两屏同款守卫——与旧排除式逐位同值)
flow.open(dbg)
Kit.check_quiet(not flow.badge_greyed_battle() and not flow.badge_greyed_map(),
"V6 角标谓词:浮窗开态两形制均不置灰〔迭代27 T4〕")
flow.close(dbg)
flow.open(map)
Kit.check_quiet(not flow.badge_greyed_map() and flow.badge_greyed_battle(),
"V6 角标谓词:地图屏 screen_state 地图形制不计/战斗形制计(旧排除式 [debug] 与 [level,debug] 同值面)〔迭代27 T4〕")
flow.close(map)
flow.open(a)
Kit.check_quiet(flow.badge_greyed_battle() and flow.badge_greyed_map(),
"V6 角标谓词:普通层开态两形制均置灰〔迭代27 T4〕")
flow.close(a)
# V6-4 演播后排窗挂起+边沿派发(T17 语义)
var end_ov := Control.new()
end_ov.visible = false
rows["empty"] = false
var sync_before1: int = calls["open_sync"]
flow.open_after_playback(end_ov)
Kit.check_quiet(flow._deferred == end_ov and not end_ov.visible and calls["open_sync"] == sync_before1,
"V6 演播后排窗:在播挂起含视觉(不照会上屏——零开侧同步)〔迭代27 T4〕")
rows["empty"] = true
var sync_before2: int = calls["open_sync"]
flow.pump_ended()
Kit.check_quiet(flow._deferred == null and end_ov.visible and calls["open_sync"] == sync_before2 + 1,
"V6 演播完边沿:挂起 modal 先开(队列空判定由 rows_empty_get 承载)〔迭代27 T4〕")
flow.close(end_ov)
flow.pump_ended()
Kit.check_quiet(calls["replay"] == 1, "V6 演播完边沿:无挂起→锁期点击重放派发〔迭代27 T4〕")
# 挂起兜底(他路径开新层先结挂起防永不兑现)
rows["empty"] = false
flow.open_after_playback(end_ov)
rows["empty"] = true
var other := Control.new()
var sync_before: int = calls["open_sync"]
flow.open(other)
Kit.check_quiet(end_ov.visible and flow._deferred == null and calls["open_sync"] == sync_before + 2,
"V6 挂起兜底:开新层先结挂起(T17 防永不兑现——挂起层先开+新层后开两拍)〔迭代27 T4〕")
quit(Kit.finish("modal_flow"))