using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; /// /// Pure unit tests for — no dats, no GL. /// Verifies the tree-builder: widget dispatch, Type-12 skipping, and meter child consumption. /// public class LayoutImporterTests { private static (uint, int, int) NoTex(uint _) => (0, 0, 0); // ── Test 1: Health meter element → UiMeter with correct rect ───────────── /// /// A Type-7 (meter) child element with X=5,Y=5,W=150,H=16 must produce a UiMeter /// that is findable by its id, positioned at Left=5, Width=150. /// The resolve lambda is a 1-arg Func<uint,(uint,int,int)>. /// [Fact] public void BuildFromInfos_HealthMeter_IsUiMeterAtRect() { var root = new ElementInfo { Id = 0x100005F9, Type = 3, Width = 160, Height = 58 }; var health = new ElementInfo { Id = 0x100000E6, Type = 7, X = 5, Y = 5, Width = 150, Height = 16 }; var tree = LayoutImporter.BuildFromInfos(root, new[] { health }, NoTex, null); var found = tree.FindElement(0x100000E6); Assert.IsType(found); Assert.Equal(5f, found!.Left); Assert.Equal(150f, found.Width); } // ── Test 2: Type-12 child builds a UiText; Type-3 sibling is also present ── /// /// A root with two children: one Type-12 UIElement_Text and one Type-3 container. /// The Type-12 must appear as a in the tree (transparent, /// draws nothing until a controller binds its LinesProvider); /// the Type-3 must also be present. /// [Fact] public void BuildFromInfos_Type12Child_IsSkipped_Type3Present() { var root = new ElementInfo { Id = 0x10000001, Type = 3, Width = 160, Height = 58 }; var prototype = new ElementInfo { Id = 0x20000001, Type = 12, Width = 0, Height = 0 }; var container = new ElementInfo { Id = 0x20000002, Type = 3, Width = 100, Height = 20 }; var tree = LayoutImporter.BuildFromInfos(root, new[] { prototype, container }, NoTex, null); // Type-12 is now a UiText (transparent, no lines) — present in the tree. Assert.IsType(tree.FindElement(0x20000001)); // Type-3 must also be present. Assert.NotNull(tree.FindElement(0x20000002)); } // ── Test 3: Meter consumes Type-3 slice children — child ids not in byId ──── /// /// A meter (Type 7) whose children are ONLY the 3-slice back/front containers (Type 3). /// The meter itself must be findable; its Type-3 children must NOT appear as separate /// nodes in the tree (Fix 5: only non-Type-3 children are built as separate widgets). /// [Fact] public void BuildFromInfos_MeterWithSliceChildren_MeterPresent_SliceChildrenNotInTree() { const uint MeterId = 0x100000E6u; const uint BackLayerId = 0x100000E7u; const uint FrontLayerId = 0x00000002u; // Build a minimal meter with back + front containers, each with 3 slice children. var backContainer = BuildSliceContainer(BackLayerId, ReadOrder: 0, l: 0x0600747Eu, t: 0x0600747Fu, r: 0x06007480u); var frontContainer = BuildSliceContainer(FrontLayerId, ReadOrder: 1, l: 0x06007481u, t: 0x06007482u, r: 0x06007483u); var meter = new ElementInfo { Id = MeterId, Type = 7, Width = 150, Height = 16 }; meter.Children.Add(backContainer); meter.Children.Add(frontContainer); var root = new ElementInfo { Id = 0x100005F9, Type = 3, Width = 160, Height = 58 }; var tree = LayoutImporter.BuildFromInfos(root, new[] { meter }, NoTex, null); // The meter widget is present. Assert.IsType(tree.FindElement(MeterId)); // The meter's Type-3 slice children are NOT separate UiElement nodes. Assert.Null(tree.FindElement(BackLayerId)); Assert.Null(tree.FindElement(FrontLayerId)); // The UiMeter itself has no UiElement children (all children were Type-3, consumed). var uiMeter = (UiMeter)tree.FindElement(MeterId)!; Assert.Empty(uiMeter.Children); } // ── Test 5: Fix 5 — meter text children (Type-12) are built and registered ─ /// /// Fix 5: a meter (Type 7) with BOTH Type-3 slice containers AND a Type-12 text /// child. The Type-3 containers must be consumed (not in byId); the Type-12 text /// child must be built as a UiText, registered in byId, and attached as a UiElement /// child of the meter (so it renders as an overlay). /// [Fact] public void BuildFromInfos_MeterWithTextChild_TextChildInTreeAsUiTextChildOfMeter() { const uint MeterId = 0x10000236u; const uint BackId = 0x10000237u; // Type-3 slice const uint FrontId = 0x10000238u; // Type-3 slice const uint LabelId = 0x10000239u; // Type-12 text child const uint ValueId = 0x1000023Au; // Type-12 text child var backContainer = BuildSliceContainer(BackId, ReadOrder: 0, l: 0x0600747Eu, t: 0x0600747Fu, r: 0x06007480u); var frontContainer = BuildSliceContainer(FrontId, ReadOrder: 1, l: 0x06007481u, t: 0x06007482u, r: 0x06007483u); // Two Type-12 text children (the XP "label" and "value" overlay). var labelInfo = new ElementInfo { Id = LabelId, Type = 12, X = 0, Y = 0, Width = 120, Height = 13 }; var valueInfo = new ElementInfo { Id = ValueId, Type = 12, X = 0, Y = 0, Width = 200, Height = 13, HJustify = HJustify.Right }; var meter = new ElementInfo { Id = MeterId, Type = 7, Width = 200, Height = 13 }; meter.Children.Add(backContainer); meter.Children.Add(frontContainer); meter.Children.Add(labelInfo); meter.Children.Add(valueInfo); var root = new ElementInfo { Id = 0x100005F9, Type = 3, Width = 210, Height = 20 }; var tree = LayoutImporter.BuildFromInfos(root, new[] { meter }, NoTex, null); // Meter is present. Assert.IsType(tree.FindElement(MeterId)); // Type-3 slice containers are NOT in byId (consumed by BuildMeter). Assert.Null(tree.FindElement(BackId)); Assert.Null(tree.FindElement(FrontId)); // Type-12 text children ARE in byId as UiText. Assert.IsType(tree.FindElement(LabelId)); Assert.IsType(tree.FindElement(ValueId)); // The UiMeter has exactly 2 UiElement children (the two text overlays). var uiMeter = (UiMeter)tree.FindElement(MeterId)!; Assert.Equal(2, uiMeter.Children.Count); Assert.All(uiMeter.Children, c => Assert.IsType(c)); // The value child should have RightAligned=true (HJustify.Right in the dat). var valueWidget = (UiText)tree.FindElement(ValueId)!; Assert.True(valueWidget.RightAligned, "Type-12 text child with HJustify.Right must build as RightAligned=true"); } // ── Test 6: Fix 5 — vitals meters (Type-3 only) are unaffected ──────────── /// /// Fix 5 regression guard: vitals health/stamina/mana meters have ONLY Type-3 slice /// children (no Type-12 text children). The meter must have zero UiElement children /// after build — VitalsController injects its number overlay at runtime, not the importer. /// [Fact] public void BuildFromInfos_VitalsMeter_NoTextChildren_MeterHasNoUiChildren() { const uint MeterId = 0x100000E6u; // vitals health meter const uint BackId = 0x100000E7u; const uint FrontId = 0x100000E8u; var backContainer = BuildSliceContainer(BackId, ReadOrder: 0, l: 0x0600747Eu, t: 0x0600747Fu, r: 0x06007480u); var frontContainer = BuildSliceContainer(FrontId, ReadOrder: 1, l: 0x06007481u, t: 0x06007482u, r: 0x06007483u); var meter = new ElementInfo { Id = MeterId, Type = 7, Width = 150, Height = 16 }; meter.Children.Add(backContainer); meter.Children.Add(frontContainer); var root = new ElementInfo { Id = 0x100005F9, Type = 3, Width = 160, Height = 58 }; var tree = LayoutImporter.BuildFromInfos(root, new[] { meter }, NoTex, null); Assert.IsType(tree.FindElement(MeterId)); // Vitals meter has no UiElement children — only Type-3 children were present, // all consumed, none built as UiText overlays. var uiMeter = (UiMeter)tree.FindElement(MeterId)!; Assert.Empty(uiMeter.Children); } // ── Test 4: Prototype-skip in BuildFromInfos ───────────────────────────── /// /// When one top-level element is referenced as a BaseElement by a sibling /// (mirroring the toolbar slot prototype pattern), and the prototype element /// has no own state media, the importer must NOT produce a widget for the /// prototype id (FindElement returns null), but MUST produce the derived element. /// /// NOTE: This test exercises (the pure /// layer), where prototype detection is done by inspecting the pre-resolved /// ElementInfo tree rather than the raw dat ElementDesc. The pure layer skips /// an element if its Id is in a sibling's (or child's) Children chain /// as a BaseElement — but actually the pure layer has no BaseElement knowledge /// at this stage (that's resolved before Build). The prototype-skip in the real /// world occurs in ImportInfos (the dat shell), BEFORE calling Build. /// /// This test verifies the INVARIANT that holds AFTER ImportInfos filters prototypes: /// a pure template element that was skipped is absent from FindElement, while the /// derived element (which inherited from it) IS present. /// /// We model this by simply NOT adding the prototype to the ElementInfo tree passed /// to BuildFromInfos — as if ImportInfos already filtered it out. /// [Fact] public void BuildFromInfos_PrototypeSkipped_DerivedPresent_PrototypeAbsent() { // Simulate what ImportInfos does AFTER filtering: the prototype 0xBBB00001 is // absent (already skipped by ImportInfos), the derived element 0xCCC00001 is // present with its own media inherited from the prototype. var root = new ElementInfo { Id = 0x10000001, Type = 3, Width = 200, Height = 100 }; // The derived element has its own size + media (prototype was merged into it already). var derived = new ElementInfo { Id = 0xCCC00001u, Type = 0x10000031u, // UIElement_ItemList (toolbar slot type) X = 10, Y = 10, Width = 32, Height = 32, }; derived.StateMedia[""] = (0x06001234u, 1); // Only the derived element appears in the tree (prototype was filtered by ImportInfos). var tree = LayoutImporter.BuildFromInfos(root, new[] { derived }, NoTex, null); // The derived element is present in the built tree. Assert.NotNull(tree.FindElement(0xCCC00001u)); // The prototype id is NOT in the tree (was never added). Assert.Null(tree.FindElement(0xBBB00001u)); } // ── Helpers ─────────────────────────────────────────────────────────────── private static ElementInfo BuildSliceContainer(uint id, uint ReadOrder, uint l, uint t, uint r) { var c = new ElementInfo { Id = id, Type = 3, ReadOrder = ReadOrder }; c.Children.Add(new ElementInfo { X = 0, StateMedia = { [""] = (l, 1) } }); c.Children.Add(new ElementInfo { X = 10, StateMedia = { [""] = (t, 1) } }); c.Children.Add(new ElementInfo { X = 140, StateMedia = { [""] = (r, 1) } }); return c; } }