summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P44DialogTests.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 19:29:27 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 19:29:27 +0800
commite28b3c102f2a0b387a4df84f053306d71e5e41ba (patch)
tree0b3b13b10bba4283dde3aba60cc0ec42495d6b82 /src/LivingVillage.Desktop.Tests/P44DialogTests.fs
parentd671335d687496f393dbad63950707cabbf24e5b (diff)
downloadliving-village-e28b3c102f2a0b387a4df84f053306d71e5e41ba.tar.gz
p44: 对话框/文字弹层 — 完整背景框+中文断行换行+分页翻页+Esc关闭+输入隔离(原创32x64点阵头像)
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P44DialogTests.fs')
-rw-r--r--src/LivingVillage.Desktop.Tests/P44DialogTests.fs123
1 files changed, 123 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/P44DialogTests.fs b/src/LivingVillage.Desktop.Tests/P44DialogTests.fs
new file mode 100644
index 0000000..a42be5c
--- /dev/null
+++ b/src/LivingVillage.Desktop.Tests/P44DialogTests.fs
@@ -0,0 +1,123 @@
+namespace LivingVillage.Desktop.Tests
+
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open LivingVillage.Kernel
+open LivingVillage.Kernel.Sim
+open LivingVillage.Desktop
+
+/// P44 对话框/文字弹层回归:中文断行、分页边界、输入隔离、Esc 关闭、提示隐藏、原创头像。
+[<TestClass>]
+type P44DialogTests () =
+
+ let world = Sim.initialWorldN 42UL 4
+
+ let assertEqualMsg (expected: 'a) (actual: 'a) (message: string) =
+ Assert.IsTrue((expected = actual), sprintf "%s (expected %A, actual %A)" message expected actual)
+
+ // ---- 中文正确断行 ----
+
+ [<TestMethod>]
+ member _.WrapBreaksChinesePerCharacterNotPerByte () =
+ // 每个 CJK 字宽 = 8 * textScale = 16px;宽 64px 恰好容纳 4 字。
+ assertEqualMsg [ "互动村民"; "求助" ] (DialogOverlay.wrapText 64 "互动村民求助") "按字断行"
+ // 单字过宽时也必须能断,不得死循环或丢字。
+ assertEqualMsg [ "互"; "动"; "村" ] (DialogOverlay.wrapText 10 "互动村") "超窄宽度逐字断行"
+
+ [<TestMethod>]
+ member _.WrapKeepsLatinWordsWholeAndHonoursNewlines () =
+ // "hello" = 5*6*2 = 60px;宽 70px 只能放一个词。
+ assertEqualMsg [ "hello"; "world" ] (DialogOverlay.wrapText 70 "hello world") "拉丁词不劈开"
+ assertEqualMsg [ "ab"; ""; "cd" ] (DialogOverlay.wrapText 200 "ab\n\ncd") "显式换行保留空行"
+ assertEqualMsg [ "" ] (DialogOverlay.wrapText 200 "") "空串给一行空"
+ for line in DialogOverlay.wrapText 64 "互动村民闲聊求助交易" do
+ Assert.IsTrue(
+ FloatingPrompt.measureWidth line DialogOverlay.textScale <= 64,
+ sprintf "换行后每行须不超宽:%s" line)
+
+ // ---- 分页 ----
+
+ [<TestMethod>]
+ member _.PagesChunkBodyByLinesPerPage () =
+ let texts = [ "a"; "b"; "c"; "d"; "e" ]
+ let model = DialogOverlay.create "标题" texts 1280 false
+ assertEqualMsg 2 (DialogOverlay.pageCount model) "5 行按每页 4 行切成 2 页"
+ let pages = model.Pages
+ assertEqualMsg 4 pages.[0].Length "首页 4 行"
+ assertEqualMsg 1 pages.[1].Length "次页 1 行"
+
+ [<TestMethod>]
+ member _.NextPageStopsAtLastPageWithoutCrash () =
+ let model = DialogOverlay.create "标题" [ "a"; "b"; "c"; "d"; "e" ] 1280 false
+ // 翻到最后一页。
+ let mutable current = model
+ let mutable guard = 0
+ while (DialogOverlay.nextPage current).IsSome && guard < 100 do
+ current <- (DialogOverlay.nextPage current).Value
+ guard <- guard + 1
+ let last = DialogOverlay.view current
+ assertEqualMsg last.PageCount last.PageNumber "停在最后一页"
+ Assert.IsFalse(last.HasNext, "最后一页没有下一页")
+ // 再翻不崩、保持最后一页。
+ assertEqualMsg None (DialogOverlay.nextPage current) "最后一页再翻返回 None"
+ assertEqualMsg current { current with Page = current.Page } "最后一页再翻不改状态"
+ assertEqualMsg None (DialogOverlay.prevPage { model with Page = 0 }) "首页再往回返回 None"
+
+ // ---- 输入隔离 / 关闭 / 提示隐藏 ----
+
+ [<TestMethod>]
+ member _.OverlayBlocksWorldInteractionAndHidesPrompt () =
+ Assert.IsTrue(DialogOverlay.blocksWorldInteraction true, "弹层开启须禁止世界交互穿透")
+ Assert.IsFalse(DialogOverlay.blocksWorldInteraction false, "弹层关闭不阻断")
+ Assert.IsFalse(DialogOverlay.promptVisible true, "弹层开启时世界交互提示须隐藏")
+ Assert.IsTrue(DialogOverlay.promptVisible false, "弹层关闭后提示恢复")
+ // 对照 P43 hotbar 的输入隔离:菜单/面板打开时世界输入不允许。
+ Assert.IsFalse(M5Interaction.worldInputAllowed { M5Interaction.initial with Panel = DialoguePanel })
+
+ [<TestMethod>]
+ member _.EscCloseDropsOverlayAndRestoresWorldPrompt () =
+ let model = DialogOverlay.create "标题" [ "a" ] 1280 true
+ let opened = Some model
+ Assert.IsTrue(DialogOverlay.blocksWorldInteraction opened.IsSome, "开启时阻断")
+ let closed = DialogOverlay.close opened
+ Assert.IsTrue(closed.IsNone, "关闭后弹层消失")
+ Assert.IsFalse(DialogOverlay.blocksWorldInteraction closed.IsSome, "关闭后不阻断")
+ Assert.IsTrue(DialogOverlay.promptVisible closed.IsSome, "关闭后世界提示恢复")
+ // Esc 关弹层 = 真实 ClosePanel:DialoguePanel 回到 WorldPanel,提示随后可刷新出来。
+ let view = { M5Interaction.initial with Panel = DialoguePanel }
+ let _, closedView = M5Interaction.apply M5Command.ClosePanel world view
+ assertEqualMsg M5Panel.WorldPanel closedView.Panel "Esc/ClosePanel 回到世界面板"
+ let refreshed = M5Interaction.refreshPrompt world closedView
+ Assert.IsTrue(M5Interaction.worldInputAllowed refreshed, "关闭后世界输入恢复")
+
+ [<TestMethod>]
+ member _.WorldPromptIsHiddenWhileDialogPanelOpen () =
+ let view = { M5Interaction.initial with Panel = DialoguePanel; Prompt = Some "E 交谈" }
+ let refreshed = M5Interaction.refreshPrompt world view
+ Assert.IsTrue(refreshed.Prompt.IsNone, "对话弹层开启时 worldview 提示须为 None")
+ Assert.IsTrue(refreshed.PromptTargetPos.IsNone, "对话弹层开启时提示锚点也清空")
+
+ // ---- 完整背景框与原创头像 ----
+
+ [<TestMethod>]
+ member _.DialogFrameFitsViewportAndHoldsAvatar () =
+ let model = DialogOverlay.create "互动:村民 1" [ "1 闲聊"; "2 求助"; "3 交易"; "4 开玩笑"; "ESC 关闭" ] 1280 true
+ let layout = DialogOverlay.rect 1280 720 model
+ Assert.IsTrue(layout.X >= 0 && layout.Y >= 0, "弹层不得越出视口")
+ Assert.IsTrue(layout.X + layout.Width <= 1280 && layout.Y + layout.Height <= 720, "弹层不得越出视口")
+ Assert.IsTrue(layout.Width >= DialogOverlay.minFrameWidth, "背景框有足够宽度")
+ let avatar = DialogOverlay.avatarRect layout
+ Assert.IsTrue(avatar.X >= layout.X && avatar.Y >= layout.Y, "头像须在框内")
+ Assert.IsTrue(
+ avatar.X + avatar.Width <= layout.X + layout.Width && avatar.Y + avatar.Height <= layout.Y + layout.Height,
+ "头像须在框内")
+ assertEqualMsg DialogOverlay.avatarWidth avatar.Width "头像宽 32"
+ assertEqualMsg DialogOverlay.avatarHeight avatar.Height "头像高 64"
+
+ [<TestMethod>]
+ member _.OriginalAvatarPatternIsWellFormedDotMatrix () =
+ assertEqualMsg DialogOverlay.avatarRows DialogOverlay.avatarPattern.Length "头像行数"
+ for row in DialogOverlay.avatarPattern do
+ assertEqualMsg DialogOverlay.avatarColumns row.Length "头像行宽"
+ Assert.IsTrue(DialogOverlay.avatarPattern |> List.exists (fun r -> r.Contains "1"), "头像须有实心像素")
+ assertEqualMsg DialogOverlay.avatarWidth (DialogOverlay.avatarColumns * DialogOverlay.avatarScale) "头像宽=列×scale"
+ assertEqualMsg DialogOverlay.avatarHeight (DialogOverlay.avatarRows * DialogOverlay.avatarScale) "头像高=行×scale"