namespace LivingVillage.Desktop.Tests open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Kernel open LivingVillage.Kernel.Sim open LivingVillage.Desktop /// P44 对话框/文字弹层回归:中文断行、分页边界、输入隔离、Esc 关闭、提示隐藏、原创头像。 [] 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) // ---- 中文正确断行 ---- [] member _.WrapBreaksChinesePerCharacterNotPerByte () = // 每个 CJK 字宽 = 8 * textScale = 16px;宽 64px 恰好容纳 4 字。 assertEqualMsg [ "互动村民"; "求助" ] (DialogOverlay.wrapText 64 "互动村民求助") "按字断行" // 单字过宽时也必须能断,不得死循环或丢字。 assertEqualMsg [ "互"; "动"; "村" ] (DialogOverlay.wrapText 10 "互动村") "超窄宽度逐字断行" [] 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) // ---- 分页 ---- [] 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 行" [] 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" // ---- 输入隔离 / 关闭 / 提示隐藏 ---- [] 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 }) [] 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, "关闭后世界输入恢复") [] 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, "对话弹层开启时提示锚点也清空") // ---- 完整背景框与原创头像 ---- [] 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" [] 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"