using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; /// /// Unit tests for — the Attributes-tab controller that /// binds the REAL importer-mounted header + 9-row list + footer elements (no created overlay). /// Pure data wiring against fake layouts; no dats, no GL. /// /// Pass 1 tests verify header/XP meter/attribute list/footer State-A binding. /// Pass 2 tests verify: (a) row click → selection toggle; (b) footer State-B content; /// (c) raise-button affordability; (d) tab button states. /// public class CharacterStatControllerTests { // ── Header labels bind to the sheet ────────────────────────────────────── [Fact] public void Bind_SetsNameLabel() { var name = new UiText(); var layout = Fake((CharacterStatController.NameId, name)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal("Studio Player", name.LinesProvider()[0].Text); } [Fact] public void Bind_SetsLevelLabel() { var level = new UiText(); var layout = Fake((CharacterStatController.LevelId, level)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal("126", level.LinesProvider()[0].Text); } [Fact] public void Bind_SetsHeritageAndPkLabels() { var heritage = new UiText(); var pk = new UiText(); var layout = Fake((CharacterStatController.HeritageId, heritage), (CharacterStatController.PkStatusId, pk)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal("Aluvian Heritage", heritage.LinesProvider()[0].Text); Assert.Equal("Non-Player Killer", pk.LinesProvider()[0].Text); } // ── XP meter fill ──────────────────────────────────────────────────────── [Fact] public void Bind_SetsXpMeterFill() { var meter = new UiMeter(); var layout = Fake((CharacterStatController.XpMeterId, meter)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var fill = meter.Fill(); Assert.NotNull(fill); Assert.True(System.MathF.Abs(fill!.Value - 0.63f) < 0.001f, $"expected ~0.63, got {fill}"); } // ── Attribute list — 9 rows in list box 0x1000023D ─────────────────────── [Fact] public void Bind_AttributeList_Has9Rows() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); // Rows are UiClickablePanel which inherits UiPanel, so OfType matches. var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); } [Fact] public void Bind_AttributeList_RowsAreClickablePanels() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); // All rows must have an OnClick wired (not null) and ClickThrough = false. foreach (var row in rows) { Assert.NotNull(row.OnClick); Assert.False(row.ClickThrough, "clickable row must accept pointer hits"); } } [Fact] public void Bind_AttributeList_EachRowHasRightAlignedValueLabel() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); foreach (var row in rows) { var texts = row.Children.OfType().ToList(); Assert.True(texts.Count >= 2, "each row must have at least name + value UiText"); var valueEl = texts[^1]; Assert.True(valueEl.RightAligned, "value label must be RightAligned"); } } [Fact] public void Bind_AttributeList_RowNamesInRetailOrder() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); string[] expectedNames = { "Strength", "Endurance", "Coordination", "Quickness", "Focus", "Self", "Health", "Stamina", "Mana", }; for (int i = 0; i < 9; i++) { var texts = rows[i].Children.OfType().ToList(); Assert.True(texts.Count >= 2, $"row {i} must have name + value"); string rowName = texts[1].LinesProvider()[0].Text; Assert.Equal(expectedNames[i], rowName); } } [Fact] public void Bind_AttributeList_RowValues_AttributeIntegersAndVitalsCurMax() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); string ValueOf(UiPanel row) => row.Children.OfType().ToList()[^1].LinesProvider()[0].Text; Assert.Equal("200", ValueOf(rows[0])); // Strength Assert.Equal("10", ValueOf(rows[1])); // Endurance Assert.Equal("10", ValueOf(rows[2])); // Coordination Assert.Equal("200", ValueOf(rows[3])); // Quickness Assert.Equal("10", ValueOf(rows[4])); // Focus Assert.Equal("10", ValueOf(rows[5])); // Self Assert.Equal("5/5", ValueOf(rows[6])); // Health Assert.Equal("10/10", ValueOf(rows[7])); // Stamina Assert.Equal("10/10", ValueOf(rows[8])); // Mana } [Fact] public void Bind_AttributeList_IconHasBackgroundSpriteWhenResolverProvided() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: id => (id, 16, 16)); var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); var iconEl = rows[0].Children.OfType().First(); Assert.Equal(0x060002C8u, iconEl.BackgroundSprite); var healthIcon = rows[6].Children.OfType().First(); Assert.Equal(0x06004C3Bu, healthIcon.BackgroundSprite); } [Fact] public void Bind_AttributeList_IconBackgroundSprite_ZeroWhenNoResolver() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: null); var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); foreach (var row in rows) { var iconEl = row.Children.OfType().First(); Assert.Equal(0u, iconEl.BackgroundSprite); } } // ── Footer State A ──────────────────────────────────────────────────────── [Fact] public void Bind_FooterStateA_TitleIsSelectPrompt() { var title = new UiText(); var layout = Fake((CharacterStatController.FooterTitleId, title)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); // Initial state (nothing selected): State-A title. Assert.Equal("Select an Attribute to Improve", title.LinesProvider()[0].Text); } [Fact] public void Bind_FooterStateA_Line1LabelIsSkillCreditsAvailable() { var lbl = new UiText(); var layout = Fake((CharacterStatController.FooterLine1Label, lbl)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal("Skill Credits Available:", lbl.LinesProvider()[0].Text); } [Fact] public void Bind_FooterStateA_Line1ValueIsSkillCredits() { var val = new UiText(); var layout = Fake((CharacterStatController.FooterLine1Value, val)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal("96", val.LinesProvider()[0].Text); } [Fact] public void Bind_FooterStateA_Line2LabelIsUnassignedExperience() { var lbl = new UiText(); var layout = Fake((CharacterStatController.FooterLine2Label, lbl)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal("Unassigned Experience:", lbl.LinesProvider()[0].Text); } [Fact] public void Bind_FooterStateA_Line2ValueIsUnassignedXp() { var val = new UiText(); var layout = Fake((CharacterStatController.FooterLine2Value, val)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var expected = (87_757_321_741L).ToString("N0"); Assert.Equal(expected, val.LinesProvider()[0].Text); } // ── Pass 2: Row selection → Footer State B ─────────────────────────────── [Fact] public void RowClick_SelectRow4Focus_FooterStateBShowsFocusTitle() { // Focus is index 4 in AttrRows. SampleData Focus = 10. Cost = 110. var title = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterTitleId, title), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); // Simulate a click on row 4 (Focus). var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); rows[4].OnClick!(); // Footer title should now be "Focus: 10". Assert.Equal("Focus: 10", title.LinesProvider()[0].Text); } [Fact] public void RowClick_SelectRow4Focus_FooterLine1LabelIsExperienceToRaise() { var lbl = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterLine1Label, lbl), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); list.Children.OfType().ToList()[4].OnClick!(); Assert.Equal("Experience To Raise:", lbl.LinesProvider()[0].Text); } [Fact] public void RowClick_SelectRow4Focus_FooterLine1ValueIsRaiseCost() { var val = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterLine1Value, val), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); list.Children.OfType().ToList()[4].OnClick!(); // Focus raise cost = 110 (SampleData fixture). Assert.Equal((110L).ToString("N0"), val.LinesProvider()[0].Text); } [Fact] public void RowClick_SelectRow4Focus_FooterLine2LabelIsUnassignedExperience() { var lbl = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterLine2Label, lbl), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); list.Children.OfType().ToList()[4].OnClick!(); Assert.Equal("Unassigned Experience:", lbl.LinesProvider()[0].Text); } [Fact] public void RowClick_SelectRow4Focus_FooterLine2ValueIsUnassignedXp() { var val = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterLine2Value, val), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); list.Children.OfType().ToList()[4].OnClick!(); // UnassignedXp = 87_757_321_741L var expected = (87_757_321_741L).ToString("N0"); Assert.Equal(expected, val.LinesProvider()[0].Text); } // ── Pass 2: Toggle deselects ────────────────────────────────────────────── [Fact] public void RowClick_ToggleSameRow_ReturnsToFooterStateA() { var title = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterTitleId, title), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); // Select Focus (row 4). rows[4].OnClick!(); Assert.Equal("Focus: 10", title.LinesProvider()[0].Text); // Click the same row again → deselect. rows[4].OnClick!(); Assert.Equal("Select an Attribute to Improve", title.LinesProvider()[0].Text); } [Fact] public void RowClick_SwitchRow_UpdatesToNewRow() { var title = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterTitleId, title), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); // Select Endurance (row 1, value=10). rows[1].OnClick!(); Assert.Equal("Endurance: 10", title.LinesProvider()[0].Text); // Select Self (row 5, value=10). rows[5].OnClick!(); Assert.Equal("Self: 10", title.LinesProvider()[0].Text); } // ── Pass 2: Row highlight ───────────────────────────────────────────────── [Fact] public void RowClick_SelectRow_HighlightsSelectedAndClearsOthers() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); // All rows start transparent. Assert.All(rows, r => Assert.Equal(0f, r.BackgroundColor.W)); // Select row 2 (Coordination). rows[2].OnClick!(); Assert.NotEqual(0f, rows[2].BackgroundColor.W); // highlighted Assert.Equal(0f, rows[0].BackgroundColor.W); // others cleared Assert.Equal(0f, rows[1].BackgroundColor.W); } [Fact] public void RowClick_Deselect_ClearsHighlight() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); rows[2].OnClick!(); // select rows[2].OnClick!(); // deselect Assert.Equal(0f, rows[2].BackgroundColor.W); } // ── Pass 2: Raise button affordability ─────────────────────────────────── [Fact] public void RaiseButtons_InitiallyHidden() { var btn1 = MakeButton(); var btn10 = MakeButton(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.RaiseOneId, btn1), (CharacterStatController.RaiseTenId, btn10), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.False(btn1.Visible, "raise×1 must start hidden"); Assert.False(btn10.Visible, "raise×10 must start hidden"); } [Fact] public void RaiseButtons_AffordableRow_ShowsNormalState() { // Focus row (index 4) cost=110, UnassignedXp=87_757_321_741 → affordable. var btn1 = MakeButton(); var btn10 = MakeButton(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.RaiseOneId, btn1), (CharacterStatController.RaiseTenId, btn10), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); list.Children.OfType().ToList()[4].OnClick!(); // select Focus Assert.True(btn1.Visible, "raise×1 visible on selection"); Assert.True(btn10.Visible, "raise×10 visible on selection"); Assert.Equal("Normal", btn1.ActiveState); Assert.Equal("Normal", btn10.ActiveState); } [Fact] public void RaiseButtons_MaxedRow_ShowsGhostedState() { // Strength row (index 0) cost=0 → disabled. UnassignedXp is irrelevant. var btn1 = MakeButton(); var btn10 = MakeButton(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.RaiseOneId, btn1), (CharacterStatController.RaiseTenId, btn10), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); list.Children.OfType().ToList()[0].OnClick!(); // select Strength (cost=0) Assert.True(btn1.Visible, "raise button visible even when disabled"); Assert.Equal("Ghosted", btn1.ActiveState); Assert.Equal("Ghosted", btn10.ActiveState); } [Fact] public void RaiseButtons_Deselect_HidesButtons() { var btn1 = MakeButton(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.RaiseOneId, btn1), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var rows = list.Children.OfType().ToList(); rows[4].OnClick!(); // select Assert.True(btn1.Visible); rows[4].OnClick!(); // deselect Assert.False(btn1.Visible, "raise button hidden after deselect"); } // ── Pass 2: Tab button states ───────────────────────────────────────────── // Tab sprites are added to layout.Root (NOT to the tab group elements), so // the tab groups keep Children.Count==0 and survive the page-visibility pass. // AddTabSpritesToRoot() adds 3 sprite UiTexts + 1 label UiText per tab to the // root when a spriteResolve is provided; nothing is added when spriteResolve=null. [Fact] public void TabButtons_NoSpriteResolve_AddsNoSpriteChildrenToRoot() { // When spriteResolve is null, AddTabSpritesToRoot is not called — no sprites added. var layout = Fake(); // empty fake layout with just the root UiPanel int rootChildCountBefore = layout.Root.Children.Count; CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: null); // No extra children added to root when spriteResolve is null. Assert.Equal(rootChildCountBefore, layout.Root.Children.Count); } [Fact] public void TabButtons_AttributesGroup_AddsOpenSpriteIdsToRoot() { // Attributes tab (isOpen=true): 3 sprite children with Open sprite ids on ROOT. // The tab group element itself has 0 children (sprites go to root, not the group). var layout = Fake(); // minimal fake CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: id => (id, 16, 16)); // Root should contain the Open sprite ids among all tab sprite children. var sprites = layout.Root.Children.OfType() .Where(t => t.BackgroundSprite != 0u).ToList(); Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D92u); // Open left-cap Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D94u); // Open center Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D96u); // Open right-cap } [Fact] public void TabButtons_SkillsAndTitles_AddClosedSpriteIdsToRoot() { // Skills + Titles tabs (isOpen=false): Closed sprite ids appear on root. var layout = Fake(); CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: id => (id, 16, 16)); var sprites = layout.Root.Children.OfType() .Where(t => t.BackgroundSprite != 0u).ToList(); Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D93u); // Closed left-cap Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D95u); // Closed center Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D97u); // Closed right-cap } [Fact] public void TabButtons_WithSpriteResolve_AddsAllThreeTabsToRoot() { // With spriteResolve: all 3 tabs inject 4 children each (3 sprites + 1 label) // = 12 sprite children total across 3 tabs on the root. var layout = Fake(); CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: id => (id, 16, 16)); // 3 tabs × 3 sprites = 9 sprite UiTexts on root. var sprites = layout.Root.Children.OfType() .Where(t => t.BackgroundSprite != 0u).ToList(); Assert.Equal(9, sprites.Count); } // ── Affordability helpers (GetRaiseCost) ────────────────────────────────── [Fact] public void GetRaiseCost_Index4Focus_Returns110() { var sheet = SampleData.SampleCharacter(); Assert.Equal(110L, CharacterStatController.GetRaiseCost(sheet, 4)); } [Fact] public void GetRaiseCost_Index0Strength_Returns0() { var sheet = SampleData.SampleCharacter(); Assert.Equal(0L, CharacterStatController.GetRaiseCost(sheet, 0)); } [Fact] public void GetRaiseCost_OutOfRange_Returns0() { var sheet = SampleData.SampleCharacter(); Assert.Equal(0L, CharacterStatController.GetRaiseCost(sheet, 99)); } // ── GetRowName helper ───────────────────────────────────────────────────── [Fact] public void GetRowName_Index0_ReturnsStrength() => Assert.Equal("Strength", CharacterStatController.GetRowName(0)); [Fact] public void GetRowName_Index4_ReturnsFocus() => Assert.Equal("Focus", CharacterStatController.GetRowName(4)); [Fact] public void GetRowName_Index6_ReturnsHealth() => Assert.Equal("Health", CharacterStatController.GetRowName(6)); [Fact] public void GetRowName_NegativeIndex_ReturnsEmpty() => Assert.Equal(string.Empty, CharacterStatController.GetRowName(-1)); // ── SampleData sanity ───────────────────────────────────────────────────── [Fact] public void SampleCharacter_SkillCredits_Is96() => Assert.Equal(96, SampleData.SampleCharacter().SkillCredits); [Fact] public void SampleCharacter_UnassignedXp_IsSet() => Assert.Equal(87_757_321_741L, SampleData.SampleCharacter().UnassignedXp); [Fact] public void SampleCharacter_AttributeRaiseCosts_HasNineEntries() { var costs = SampleData.SampleCharacter().AttributeRaiseCosts; Assert.NotNull(costs); Assert.Equal(9, costs.Length); } [Fact] public void SampleCharacter_AttributeRaiseCosts_FocusAt110() => Assert.Equal(110L, SampleData.SampleCharacter().AttributeRaiseCosts[4]); [Fact] public void SampleCharacter_AttributeRaiseCosts_StrengthAt0() => Assert.Equal(0L, SampleData.SampleCharacter().AttributeRaiseCosts[0]); // ── UiText flag sanity ──────────────────────────────────────────────────── [Fact] public void UiText_RightAligned_DefaultFalse() { var t = new UiText(); Assert.False(t.RightAligned); } [Fact] public void UiText_RightAligned_CanBeSetTrue() { var t = new UiText { RightAligned = true }; Assert.True(t.RightAligned); } // ── Header captions (new — LevelCaptionId + TotalXpLabelId) ───────────── [Fact] public void Bind_LevelCaptionId_SetsCharacterLevelText() { var caption = new UiText(); var layout = Fake((CharacterStatController.LevelCaptionId, caption)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); // 2-line caption: "Character" (top) / "Level" (bottom) so it fits the 65px element. var lines = caption.LinesProvider(); Assert.True(lines.Count >= 1, "LevelCaption must provide at least one line"); Assert.Equal("Character", lines[0].Text); Assert.False(caption.Centered, "LevelCaption must be left-justified (Centered=false)"); Assert.False(caption.RightAligned, "LevelCaption must be left-justified (RightAligned=false)"); } [Fact] public void Bind_TotalXpLabelId_SetsTotalExperienceXpText() { var lbl = new UiText(); var layout = Fake((CharacterStatController.TotalXpLabelId, lbl)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal("Total Experience (XP):", lbl.LinesProvider()[0].Text); Assert.False(lbl.Centered, "TotalXpLabel must be left-justified (Centered=false)"); Assert.False(lbl.RightAligned, "TotalXpLabel must be left-justified (RightAligned=false)"); } // ── Polish Commit 1: name white, Infinity!, white footer title ───────────── [Fact] public void Bind_NameColor_IsWhite() { // Retail: name "Horan" is WHITE, not gold. (2026-06-26 ref) var name = new UiText(); var layout = Fake((CharacterStatController.NameId, name)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var color = name.LinesProvider()[0].Color; Assert.Equal(1f, color.X, precision: 3); Assert.Equal(1f, color.Y, precision: 3); Assert.Equal(1f, color.Z, precision: 3); Assert.Equal(1f, color.W, precision: 3); } [Fact] public void RowClick_MaxedRow_FooterLine1ValueIsInfinity() { // Strength (index 0) cost=0 → "Infinity!" per retail spec. var val = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterLine1Value, val), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); list.Children.OfType().ToList()[0].OnClick!(); // Strength Assert.Equal("Infinity!", val.LinesProvider()[0].Text); } [Fact] public void RowClick_SelectedFooterTitle_IsWhite() { // State B footer title must be WHITE (retail 2026-06-26 ref). var title = new UiText(); var list = new UiPanel(); var layout = Fake( (CharacterStatController.FooterTitleId, title), (CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); list.Children.OfType().ToList()[4].OnClick!(); // Focus var color = title.LinesProvider()[0].Color; Assert.Equal(1f, color.X, precision: 3); Assert.Equal(1f, color.Y, precision: 3); Assert.Equal(1f, color.Z, precision: 3); Assert.Equal(1f, color.W, precision: 3); } [Fact] public void Bind_FooterStateA_TitleColor_IsBodyNotWhite() { // State A title is body (parchment), not white. var title = new UiText(); var layout = Fake((CharacterStatController.FooterTitleId, title)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); // Body = (0.92, 0.90, 0.82, 1.0) — check it's not pure white. var color = title.LinesProvider()[0].Color; Assert.True(color.X < 1f || color.Y < 1f || color.Z < 1f, "State-A title should be body/parchment color, not pure white"); } [Fact] public void Bind_LevelCaptionId_SetsTwoLines() { // Retail: level caption is "Character" / "Level" on two lines (not truncated). var caption = new UiText(); var layout = Fake((CharacterStatController.LevelCaptionId, caption)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); var lines = caption.LinesProvider(); Assert.Equal(2, lines.Count); Assert.Equal("Character", lines[0].Text); Assert.Equal("Level", lines[1].Text); } // ── Robustness ──────────────────────────────────────────────────────────── [Fact] public void Bind_MissingElements_DoesNotThrow() => CharacterStatController.Bind(Fake(), SampleData.SampleCharacter); // ── Helpers ────────────────────────────────────────────────────────────── /// Manufacture a minimal UiButton with a fake ElementInfo (no dat sprites). private static UiButton MakeButton() { var info = new ElementInfo { Id = 0, Type = 1 }; return new UiButton(info, static _ => (0u, 0, 0)); } private static ImportedLayout Fake(params (uint id, UiElement e)[] items) { var dict = new Dictionary(); var root = new UiPanel(); foreach (var (id, e) in items) { root.AddChild(e); dict[id] = e; } return new ImportedLayout(root, dict); } }