acdream/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs
Erik bd01a29eb2 feat(D.2b): LayoutImporter — read layout + resolve inheritance + build tree
Implements Task 5 of the LayoutDesc Importer (Plan 1 — vitals conformance).

Pure layer (BuildFromInfos / Build):
- ImportedLayout result type: UiElement root + O(1) FindElement(uint id) lookup
- BuildWidget dispatches via DatWidgetFactory.Create; skips Type-12 prototypes (null)
- Meters consume their children (DatWidgetFactory already extracted slice ids —
  adding the dat children as UiElement nodes would duplicate geometry)
- All other element types recurse children generically via AddChild

Dat shell (Import):
- Loads LayoutDesc from dats; null-safe if layout is absent
- Resolves each top-level ElementDesc to ElementInfo via Resolve():
  BaseElement/BaseLayoutId chain with (layoutId,elementId) cycle guard
- ToInfo(): reads ElementDesc scalar fields (uint → float cast) + DirectState +
  named States (UIStateId.ToString() as key)
- ReadState(): extracts first MediaDescImage (File + DrawMode) per state +
  font DID from Properties[0x1A] → ArrayBaseProperty → DataIdBaseProperty.Value
- Each sibling element gets a fresh base-chain set (siblings don't share guards)

DRW API: all members confirmed from VitalsLayoutDump.cs usings — no
adjustments needed: LayoutDesc in DBObjs; ElementDesc/StateDesc/MediaDescImage/
ArrayBaseProperty/DataIdBaseProperty in Types; DrawModeType/UIStateId in Enums.

Tests (3/3 green):
- BuildFromInfos_HealthMeter_IsUiMeterAtRect — Type-7 child → UiMeter, Left=5, Width=150
- BuildFromInfos_Type12Child_IsSkipped_Type3Present — prototype absent, container present
- BuildFromInfos_MeterWithChildren_MeterPresent_ChildrenNotInTree — meter findable,
  both dat-children absent, UiMeter.Children empty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 13:52:50 +02:00

105 lines
4.7 KiB
C#

using AcDream.App.UI;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Pure unit tests for <see cref="LayoutImporter.BuildFromInfos"/> — no dats, no GL.
/// Verifies the tree-builder: widget dispatch, Type-12 skipping, and meter child consumption.
/// </summary>
public class LayoutImporterTests
{
private static (uint, int, int) NoTex(uint _) => (0, 0, 0);
// ── Test 1: Health meter element → UiMeter with correct rect ─────────────
/// <summary>
/// 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&lt;uint,(uint,int,int)&gt;.
/// </summary>
[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<UiMeter>(found);
Assert.Equal(5f, found!.Left);
Assert.Equal(150f, found.Width);
}
// ── Test 2: Type-12 child is skipped; Type-3 sibling is present ──────────
/// <summary>
/// A root with two children: one Type-12 style prototype and one Type-3 container.
/// The Type-12 must be absent from the tree (FindElement returns null);
/// the Type-3 must be present.
/// </summary>
[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 must be absent.
Assert.Null(tree.FindElement(0x20000001));
// Type-3 must be present.
Assert.NotNull(tree.FindElement(0x20000002));
}
// ── Test 3: Meter consumes its children — child ids not in byId ──────────
/// <summary>
/// A meter (Type 7) whose children are the 3-slice back/front containers.
/// The meter itself must be findable; its direct children must NOT appear as
/// separate nodes in the tree (meters own their children, not the generic tree).
/// </summary>
[Fact]
public void BuildFromInfos_MeterWithChildren_MeterPresent_ChildrenNotInTree()
{
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<UiMeter>(tree.FindElement(MeterId));
// The meter's dat-children are NOT separate UiElement nodes.
Assert.Null(tree.FindElement(BackLayerId));
Assert.Null(tree.FindElement(FrontLayerId));
// The UiMeter itself has no Ui children (meters consume their children internally).
var uiMeter = (UiMeter)tree.FindElement(MeterId)!;
Assert.Empty(uiMeter.Children);
}
// ── 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;
}
}