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. /// 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 ─────────────────────── /// Bind adds exactly 9 UiPanel row containers to the list box. [Fact] public void Bind_AttributeList_Has9Rows() { 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); } /// /// Every row must have a right-aligned value UiText child (the last UiText in /// the row), confirming the RightAligned path was wired. /// [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) { // Last UiText in each row is the value (right-aligned). var texts = row.Children.OfType().ToList(); Assert.True(texts.Count >= 2, "each row must have at least name + value UiText"); var valueEl = texts[^1]; // last = value Assert.True(valueEl.RightAligned, "value label must be RightAligned"); } } /// /// Retail display order: Strength, Endurance, Coordination, Quickness, Focus, Self, /// Health, Stamina, Mana (spec §1). Name is the SECOND UiText child of each row. /// [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++) { // Name UiText = second UiText in the row (after the icon UiText). 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; // index 1 = name (0 = icon) Assert.Equal(expectedNames[i], rowName); } } /// /// Attribute row values are plain integers; vital row values are "cur/max". /// Validates against SampleData fixture: Str=200, Quick=200, rest=10; /// Health=5/5, Stamina=10/10, Mana=10/10. /// [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(); // Helper: value text is the last UiText in the row. string ValueOf(UiPanel row) => row.Children.OfType().ToList()[^1].LinesProvider()[0].Text; // Attribute rows 0-5. 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 // Vital rows 6-8: "cur/max" format. Assert.Equal("5/5", ValueOf(rows[6])); // Health Assert.Equal("10/10", ValueOf(rows[7])); // Stamina Assert.Equal("10/10", ValueOf(rows[8])); // Mana } /// /// When spriteResolve is provided the icon UiText must have BackgroundSprite set /// to the correct DataID; when null the BackgroundSprite must be 0. /// [Fact] public void Bind_AttributeList_IconHasBackgroundSpriteWhenResolverProvided() { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); // Non-null resolver — just returns a fake handle for any id. CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: id => (id, 16, 16)); // fake: handle == id var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); // First icon = Strength row; expected DataID 0x060002C8. var iconEl = rows[0].Children.OfType().First(); Assert.Equal(0x060002C8u, iconEl.BackgroundSprite); // Vital (Health) = row 6; expected DataID 0x06004C3B. 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_TitleEmpty() { var title = new UiText(); var layout = Fake((CharacterStatController.FooterTitleId, title)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal(string.Empty, title.LinesProvider()[0].Text); } [Fact] public void Bind_FooterStateA_Line1LabelEmpty() { var lbl = new UiText(); var layout = Fake((CharacterStatController.FooterLine1Label, lbl)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal(string.Empty, lbl.LinesProvider()[0].Text); } [Fact] public void Bind_FooterStateA_Line1ValueIsSelectPrompt() { var val = new UiText(); var layout = Fake((CharacterStatController.FooterLine1Value, val)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal("Select an Attribute to Improve", val.LinesProvider()[0].Text); } [Fact] public void Bind_FooterStateA_Line2LabelEmpty() { var lbl = new UiText(); var layout = Fake((CharacterStatController.FooterLine2Label, lbl)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); Assert.Equal(string.Empty, lbl.LinesProvider()[0].Text); } [Fact] public void Bind_FooterStateA_Line2ValueIsSkillCredits() { var val = new UiText(); var layout = Fake((CharacterStatController.FooterLine2Value, val)); CharacterStatController.Bind(layout, SampleData.SampleCharacter); // SampleData.SampleCharacter().SkillCredits == 96 Assert.Equal("96", val.LinesProvider()[0].Text); } // ── SkillCredits propagation through SampleData ─────────────────────────── [Fact] public void SampleCharacter_SkillCredits_Is96() { Assert.Equal(96, SampleData.SampleCharacter().SkillCredits); } // ── UiText.RightAligned — unit-level draw mode flag ────────────────────── [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); } // ── Robustness: a partial layout (missing ids) must not throw ──────────── [Fact] public void Bind_MissingElements_DoesNotThrow() => CharacterStatController.Bind(Fake(), SampleData.SampleCharacter); // ── Helper ───────────────────────────────────────────────────────────── 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); } }