acdream/tests/AcDream.App.Tests/UI/Layout/VitalsBindingTests.cs
Erik 89626cd400 feat(D.2b): vitals numbers as UiText (widget-generalization Task 8)
The vitals cur/max numbers now render through the generic UiText widget — retail
gmVitalsUI uses UIElement_Text for them, not a meter-internal label. VitalsController
attaches a centered, non-interactive UiText child to each meter and stops the meter
drawing its own label (UiMeter.Label -> null). New UiText.Centered draws the first line
centered H+V with the SAME formula UiMeter's overlay used, so the numbers are
pixel-identical — user-confirmed in the live client.

This completes the D.2b widget-generalization pass: every chat + vitals widget is now
built generically and registered to its retail Type (Button/Field*/Menu/Meter/Scrollbar/
Text), with thin find-by-id controllers. (*Field is controller-placed; Type 3 stays
UiDatElement for chrome.)

Divergence register: AP-37 vitals-numbers-via-UiMeter.Label clause retired. Full suite:
404 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:52:42 +02:00

113 lines
4 KiB
C#

using AcDream.App.UI;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Unit tests for <see cref="VitalsController.Bind"/>: verifies that the controller
/// correctly maps element ids to UiMeter instances and wires the Fill / Label providers.
/// No dats, no GL — pure data-wiring tests.
/// </summary>
public class VitalsBindingTests
{
// ── Test 1: Health meter Fill + Label providers are bound ─────────────────
[Fact]
public void Bind_SetsHealthMeterFillFromProvider()
{
var health = new UiMeter();
var layout = FakeLayout((VitalsController.Health, health));
float hp = 0.42f;
VitalsController.Bind(layout,
healthPct: () => hp,
staminaPct: () => 1f,
manaPct: () => 1f,
healthText: () => "42/100",
staminaText: () => "",
manaText: () => "");
Assert.Equal(0.42f, health.Fill()!.Value);
// The meter no longer draws its own label; the cur/max is a centered UiText child.
Assert.Null(health.Label());
Assert.Equal("42/100", NumberText(health));
}
// ── Test 2: All three meters wired to distinct providers ──────────────────
[Fact]
public void Bind_AllThreeMeters_EachBoundToOwnProvider()
{
var health = new UiMeter();
var stamina = new UiMeter();
var mana = new UiMeter();
var layout = FakeLayout(
(VitalsController.Health, health),
(VitalsController.Stamina, stamina),
(VitalsController.Mana, mana));
VitalsController.Bind(layout,
healthPct: () => 0.25f,
staminaPct: () => 0.50f,
manaPct: () => 0.75f,
healthText: () => "25/100",
staminaText: () => "50/100",
manaText: () => "75/100");
// Each meter should reflect its own provider, not another's.
Assert.Equal(0.25f, health.Fill()!.Value);
Assert.Equal("25/100", NumberText(health));
Assert.Equal(0.50f, stamina.Fill()!.Value);
Assert.Equal("50/100", NumberText(stamina));
Assert.Equal(0.75f, mana.Fill()!.Value);
Assert.Equal("75/100", NumberText(mana));
}
// ── Test 3: Missing meter ids are silently skipped (no throw) ─────────────
[Fact]
public void Bind_MissingMeterIds_DoesNotThrow()
{
// Only Health is present; Stamina and Mana are absent from the layout.
var health = new UiMeter();
var layout = FakeLayout((VitalsController.Health, health));
// Should not throw even though Stamina/Mana are missing.
VitalsController.Bind(layout,
healthPct: () => 1f,
staminaPct: () => 1f,
manaPct: () => 1f,
healthText: () => "100/100",
staminaText: () => "100/100",
manaText: () => "100/100");
// Health was present — it should be wired.
Assert.Equal(1f, health.Fill()!.Value);
}
// ── Helpers ───────────────────────────────────────────────────────────────
/// <summary>The cur/max text from the centered <see cref="UiText"/> number that
/// <see cref="VitalsController"/> attaches as the meter's child.</summary>
private static string NumberText(UiMeter m)
{
var num = Assert.IsType<UiText>(m.Children[0]);
Assert.True(num.Centered);
var lines = num.LinesProvider();
return lines.Count > 0 ? lines[0].Text : "";
}
private static ImportedLayout FakeLayout(params (uint id, UiElement e)[] items)
{
var dict = new Dictionary<uint, UiElement>();
var root = new UiPanel();
foreach (var (id, e) in items)
{
root.AddChild(e);
dict[id] = e;
}
return new ImportedLayout(root, dict);
}
}