test(ui): AcDream.UI.Abstractions unit tests (11 tests green)

Covers the pure-logic surface of commit 1:

  VitalsVMTests
    * HealthPercent reads from CombatState.GetHealthPercent
    * safe-default 1.0 when GUID unknown / not yet set / never updated
    * SetLocalPlayerGuid reroutes lookup to the new guid (no stale cache)
    * StaminaPercent / ManaPercent are null for D.2a scope
    * ctor throws ArgumentNullException on null CombatState

  PanelContextTests
    * record-struct fields round-trip
    * value-based equality

  NullCommandBusTests
    * Publish accepts any record type without throwing
    * Instance is a true singleton

csproj template mirrors AcDream.Core.Tests (xUnit 2.9.3, Test.Sdk 17.14.1,
runner 3.1.4, coverlet 6.0.4, implicit Using Xunit). References only
AcDream.UI.Abstractions — no runtime / GL dependency, tests run fast.
This commit is contained in:
Erik 2026-04-25 00:25:26 +02:00
parent 8c64ad2eeb
commit fc03fa377b
5 changed files with 152 additions and 0 deletions

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\AcDream.UI.Abstractions\AcDream.UI.Abstractions.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,22 @@
namespace AcDream.UI.Abstractions.Tests;
public sealed class NullCommandBusTests
{
private sealed record FakeCmd(int Value);
[Fact]
public void Publish_DoesNotThrow_OnAnyRecordType()
{
var bus = NullCommandBus.Instance;
bus.Publish(new FakeCmd(42));
bus.Publish("a string command");
bus.Publish(12345);
}
[Fact]
public void Instance_IsSingleton()
{
Assert.Same(NullCommandBus.Instance, NullCommandBus.Instance);
}
}

View file

@ -0,0 +1,24 @@
namespace AcDream.UI.Abstractions.Tests;
public sealed class PanelContextTests
{
[Fact]
public void Fields_RoundTripThroughConstructor()
{
var ctx = new PanelContext(DeltaSeconds: 0.016f, Commands: NullCommandBus.Instance);
Assert.Equal(0.016f, ctx.DeltaSeconds);
Assert.Same(NullCommandBus.Instance, ctx.Commands);
}
[Fact]
public void RecordEquality_ByValue()
{
var a = new PanelContext(1f / 60f, NullCommandBus.Instance);
var b = new PanelContext(1f / 60f, NullCommandBus.Instance);
// Record-struct equality is value-based on DeltaSeconds + reference-based
// on Commands (since ICommandBus is a reference type, same instance → equal).
Assert.Equal(a, b);
}
}

View file

@ -0,0 +1,80 @@
using AcDream.Core.Combat;
using AcDream.UI.Abstractions.Panels.Vitals;
namespace AcDream.UI.Abstractions.Tests;
public sealed class VitalsVMTests
{
[Fact]
public void HealthPercent_ReturnsCombatStateValue_AfterUpdateHealth()
{
var combat = new CombatState();
uint guid = 0x5000_0042u;
combat.OnUpdateHealth(guid, 0.42f);
var vm = new VitalsVM(combat);
vm.SetLocalPlayerGuid(guid);
Assert.Equal(0.42f, vm.HealthPercent, precision: 3);
}
[Fact]
public void HealthPercent_ReturnsOne_WhenGuidUnknown()
{
var combat = new CombatState();
var vm = new VitalsVM(combat);
// No SetLocalPlayerGuid call — defaults to 0 which CombatState has never seen.
Assert.Equal(1f, vm.HealthPercent);
}
[Fact]
public void HealthPercent_ReturnsOne_WhenGuidSetButNeverUpdated()
{
var combat = new CombatState();
var vm = new VitalsVM(combat);
vm.SetLocalPlayerGuid(0xDEAD_BEEFu);
Assert.Equal(1f, vm.HealthPercent);
}
[Fact]
public void StaminaPercent_IsNull_ForD2aScope()
{
// D.2a explicitly defers Stamina until LocalPlayerState + PlayerDescription
// wiring. When that arrives VitalsVM.StaminaPercent becomes non-null and
// VitalsPanel starts drawing the Stam bar automatically.
var vm = new VitalsVM(new CombatState());
Assert.Null(vm.StaminaPercent);
}
[Fact]
public void ManaPercent_IsNull_ForD2aScope()
{
var vm = new VitalsVM(new CombatState());
Assert.Null(vm.ManaPercent);
}
[Fact]
public void SetLocalPlayerGuid_ReroutesHealthLookup_WithoutStaleCache()
{
// Simulate the realistic GameWindow flow: VM is constructed pre-login
// with GUID=0, then SetLocalPlayerGuid is called at EnterWorld.
var combat = new CombatState();
uint playerGuid = 0x5003_E219u;
combat.OnUpdateHealth(playerGuid, 0.75f);
var vm = new VitalsVM(combat);
// Before SetLocalPlayerGuid — reads GUID=0 → returns safe 1.0.
Assert.Equal(1f, vm.HealthPercent);
vm.SetLocalPlayerGuid(playerGuid);
Assert.Equal(0.75f, vm.HealthPercent, precision: 3);
}
[Fact]
public void Constructor_ThrowsOnNullCombat()
{
Assert.Throws<ArgumentNullException>(() => new VitalsVM(null!));
}
}