summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P44DialogTests.fs
blob: a42be5cdb39c051c96194e46305f3b36279b858e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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"