diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-28 19:29:27 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-28 19:29:27 +0800 |
| commit | e28b3c102f2a0b387a4df84f053306d71e5e41ba (patch) | |
| tree | 0b3b13b10bba4283dde3aba60cc0ec42495d6b82 /src/LivingVillage.Desktop.Tests | |
| parent | d671335d687496f393dbad63950707cabbf24e5b (diff) | |
| download | living-village-e28b3c102f2a0b387a4df84f053306d71e5e41ba.tar.gz | |
p44: 对话框/文字弹层 — 完整背景框+中文断行换行+分页翻页+Esc关闭+输入隔离(原创32x64点阵头像)
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/P44DialogTests.fs | 123 |
2 files changed, 124 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj index d9bbee1..68ec033 100644 --- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj +++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj @@ -23,6 +23,7 @@ <Compile Include="P40ArtTests.fs" /> <Compile Include="P42LaunchFxTests.fs" /> <Compile Include="P43HudTests.fs" /> + <Compile Include="P44DialogTests.fs" /> <Compile Include="SampleTests.fs" /> </ItemGroup> 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" |
