acdream/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
Erik 3c9fc57adb
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
fix(vendor): Slice 6 review corrections — ownership-checked retire, live slider display, drag-proof shop rows, hardened buy reservation
All nine findings from the buy-arc review, at root:

F1 the materializer's retire pass re-checks ownership (guid->vendorId
map; remove only while the live object's ContainerId still equals the
recording vendor) — buying a player-sold UNIQUE no longer deletes the
item you just purchased; the discriminating reparent-then-refresh test
pins it. F2 the cost/name display subscribes to the live split state
and shares ONE quantity computation with Buy (retail re-renders per
slider tick: RecvNotice_StackSliderChanged 0x004C4500) — the sentence
and the charge can no longer disagree. F3 shop rows never mint drag
payloads (UiItemSlot.AllowDragSource gates both IsDragSource AND
GetDragPayload — the second gate was caught by this pass's own test).
F4 sendBuy reports whether anything was sent; a null-session buy
cancels the reservation instead of leaking BusyCount forever.
F5 the retire loop snapshots, isolates per-guid observer failures, and
clears its tracking in finally and Dispose — teardown convergence can
no longer wedge. F6 auto-select is retail's unconditional
first-filtered-item shape (pc:201180-201184; the survival-check was
our invention and the comment claiming otherwise is corrected).
F7 non-stack buys clamp to quantity 1 locally (BuySingleItem
pc:201669). F8 the Add button is hard-disabled until staging exists.
F9 AP-161/162/163 rewritten to the post-fix reality.

Clean-room complete solution: 11,378 passed / 4 skipped / 0 failed.
The #350 render-ledger overflow observed this session is under
separate investigation and is NOT addressed here.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-08-07 23:12:50 +02:00

256 lines
8.6 KiB
C#

using AcDream.App.UI;
namespace AcDream.App.Tests.UI;
public class UiItemSlotTests
{
[Fact]
public void IsLeafWidget()
=> Assert.True(new UiItemSlot().ConsumesDatChildren);
[Fact]
public void DefaultEmptySprite_isToolbarBorder()
=> Assert.Equal(0x060074CFu, new UiItemSlot().EmptySprite);
[Fact]
public void DefaultWaitingSprite_isRetailGhostMesh()
=> Assert.Equal(0x0600109Au, new UiItemSlot().WaitingSprite);
[Fact]
public void Empty_whenNoItem()
{
var s = new UiItemSlot();
Assert.Equal(0u, s.ItemId);
Assert.Equal(0u, s.IconTexture);
}
[Fact]
public void SetItem_setsIdAndTexture()
{
var s = new UiItemSlot();
s.SetItem(0x5001u, 0x99u);
Assert.Equal(0x5001u, s.ItemId);
Assert.Equal(0x99u, s.IconTexture);
}
[Fact]
public void Clear_afterSetItem_resetsToEmpty()
{
var s = new UiItemSlot();
s.SetItem(0x5001u, 0x99u);
s.Clear();
Assert.Equal(0u, s.ItemId);
Assert.Equal(0u, s.IconTexture);
}
[Fact]
public void DoubleClick_invokesDoubleClicked()
{
var s = new UiItemSlot();
bool fired = false;
s.DoubleClicked = () => fired = true;
s.OnEvent(new UiEvent(0u, s, UiEventType.DoubleClick));
Assert.True(fired);
}
[Fact]
public void CatalogSlot_keeps_catalog_identity_separate_from_object_guid()
{
var slot = new UiCatalogSlot { EntryId = 1234u, CatalogIconTexture = 99u };
Assert.Equal(1234u, slot.EntryId);
Assert.Equal(0u, slot.ItemId);
Assert.False(slot.IsDragSource);
Assert.Null(slot.GetDragPayload());
}
// ── F3 (Slice 6 review): AllowDragSource ────────────────────────────────
[Fact]
public void AllowDragSource_DefaultsTrue_OccupiedSlotIsADragSource()
{
var s = new UiItemSlot();
s.SetItem(0x5001u, 0x99u);
Assert.True(s.IsDragSource);
Assert.NotNull(s.GetDragPayload());
}
[Fact]
public void AllowDragSource_False_OccupiedSlotIsNeverADragSource()
{
// A vendor row (or any future list retail excludes from
// ItemList_BeginDrag, e.g. salvage) sets this false. IsDragSource is
// the sole gate UiRoot reads at MouseDown to decide whether a
// press-and-move even becomes a drag CANDIDATE -- GetDragPayload()
// is never reached at all when this is false, so no destination
// drop handler needs to reject anything.
var s = new UiItemSlot { AllowDragSource = false };
s.SetItem(0x5001u, 0x99u);
Assert.False(s.IsDragSource);
}
// ── Shortcut number tests ────────────────────────────────────────────────
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
[Fact]
public void ShortcutNum_defaultIsMinusOne()
{
var s = new UiItemSlot();
Assert.Equal(-1, s.ShortcutNum);
}
[Fact]
public void ShortcutGhosted_defaultsFalse()
{
var s = new UiItemSlot();
Assert.False(s.ShortcutGhosted);
}
[Fact]
public void SetShortcutNum_setsIndexAndGhostedState()
{
var s = new UiItemSlot();
s.SetShortcutNum(3, ghosted: true);
Assert.Equal(3, s.ShortcutNum);
Assert.True(s.ShortcutGhosted);
}
[Fact]
public void SetShortcutNum_regularState()
{
var s = new UiItemSlot();
s.SetShortcutNum(0, ghosted: false);
Assert.Equal(0, s.ShortcutNum);
Assert.False(s.ShortcutGhosted);
}
[Fact]
public void ClearShortcutNum_setsMinusOne()
{
var s = new UiItemSlot();
s.SetShortcutNum(5, ghosted: false);
s.ClearShortcutNum();
Assert.Equal(-1, s.ShortcutNum);
}
// ── ActiveDigitArray occupancy gating (decomp UIElement_UIItem::SetShortcutNum:229481) ──
private static readonly uint[] Regular = { 0x10u, 0x11u, 0x12u };
private static readonly uint[] Ghosted = { 0x20u, 0x21u, 0x22u };
private static readonly uint[] Empty = { 0x30u, 0x31u, 0x32u };
/// <summary>
/// When ItemId == 0 (empty slot), ActiveDigitArray returns EmptyDigits regardless
/// of ShortcutGhosted. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) —
/// else branch when m_elem_Icon->m_state == 0x1000001c (empty).
/// </summary>
[Fact]
public void ActiveDigitArray_emptySlot_returnsEmptyDigits()
{
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
s.SetShortcutNum(0, ghosted: false);
// ItemId == 0 → EmptyDigits
Assert.Same(Empty, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_emptySlot_ghosted_stillReturnsEmptyDigits()
{
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
s.SetShortcutNum(0, ghosted: true);
// ItemId == 0 → EmptyDigits regardless of stance
Assert.Same(Empty, s.ActiveDigitArray());
}
/// <summary>
/// When ItemId != 0 (occupied), ActiveDigitArray returns RegularDigits or GhostedDigits
/// depending on ShortcutGhosted. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493).
/// </summary>
[Fact]
public void ActiveDigitArray_occupiedSlot_regular_returnsRegularDigits()
{
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
s.SetItem(0x5001u, 0x99u);
s.SetShortcutNum(0, ghosted: false);
Assert.Same(Regular, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_occupiedSlot_ghosted_returnsGhostedDigits()
{
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
s.SetItem(0x5001u, 0x99u);
s.SetShortcutNum(0, ghosted: true);
Assert.Same(Ghosted, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_emptySlot_nullEmptyDigits_returnsNull()
{
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = null };
s.SetShortcutNum(0, ghosted: false);
Assert.Null(s.ActiveDigitArray());
}
// ── Container-switching indicator overlays (decomp SetOpenContainerState 0x004e1200 /
// SetSelectedState 0x004e1240) ───────────────────────────────────────────────────────
[Fact]
public void Selected_defaultsFalse() => Assert.False(new UiItemSlot().Selected);
[Fact]
public void IsOpenContainer_defaultsFalse() => Assert.False(new UiItemSlot().IsOpenContainer);
[Fact]
public void SelectedSprite_default_isGreenYellowSquare()
=> Assert.Equal(0x06004D21u, new UiItemSlot().SelectedSprite);
[Fact]
public void OpenContainerSprite_default_isTriangle()
=> Assert.Equal(0x06005D9Cu, new UiItemSlot().OpenContainerSprite);
// ── Container capacity bar (decomp UpdateCapacityDisplay 0x004e16e0, element 0x10000347) ──
[Fact]
public void CapacityFill_defaultsHidden() => Assert.Equal(-1f, new UiItemSlot().CapacityFill);
[Fact]
public void CapacityBackSprite_default() => Assert.Equal(0x06004D22u, new UiItemSlot().CapacityBackSprite);
[Fact]
public void CapacityFrontSprite_default() => Assert.Equal(0x06004D23u, new UiItemSlot().CapacityFrontSprite);
[Fact]
public void ActiveCooldownSprite_selects_the_exact_one_based_step()
{
uint[] sprites =
[
0x06000001u, 0x06000002u, 0x06000003u, 0x06000004u, 0x06000005u,
0x06000006u, 0x06000007u, 0x06000008u, 0x06000009u, 0x0600000Au,
];
var slot = new UiItemSlot
{
CooldownSprites = sprites,
CooldownStepProvider = id => id == 0x5001u ? 6 : 0,
};
slot.SetItem(0x5001u, 99u);
Assert.Equal(0x06000006u, slot.ActiveCooldownSprite());
}
[Fact]
public void ActiveCooldownSprite_isHidden_for_empty_or_inactive_slot()
{
var slot = new UiItemSlot
{
CooldownSprites = Enumerable.Range(1, 10).Select(i => (uint)i).ToArray(),
CooldownStepProvider = _ => 0,
};
Assert.Equal(0u, slot.ActiveCooldownSprite());
slot.SetItem(0x5001u, 99u);
Assert.Equal(0u, slot.ActiveCooldownSprite());
}
}