using System.Collections.Generic; using System.Linq; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; namespace AcDream.App.Tests.UI.Layout; /// /// Slice 5.4 — mirrors 's /// harness shape (hand-built over /// ) for the behavioral suite, plus one /// real-dat-fixture smoke test (mirroring /// AppraisalUiControllerTests's use of ) /// that catches drift between the hardcoded ids in /// and the actual LayoutDesc 0x21000012. /// public sealed class VendorUiControllerTests { private const uint VendorGuid = 0x70000010u; private const uint ArmorItemGuid = 0x60000101u; private const uint FoodItemGuid = 0x60000102u; private const uint StackedItemGuid = 0x60000103u; private sealed class TestElement : UiElement { } [Fact] public void Bind_FromRealDatFixture_ResolvesAllRequiredControls() { ImportedLayout layout = FixtureLoader.LoadVendor(); var screen = new UiRoot { Width = 1280f, Height = 800f }; RetailWindowHandle window = RetailWindowFrame.Mount( screen, layout.Root, static _ => (0u, 0, 0), new RetailWindowFrame.Options { WindowName = "vendor-fixture-smoke", Chrome = RetailWindowChrome.Imported, Visible = false, }); VendorUiController? controller = VendorUiController.Bind( layout, new VendorState(), window, static (_, _, _, _, _) => 0u); Assert.NotNull(controller); } private sealed class Harness { public readonly VendorState State = new(); public readonly UiRoot Screen = new() { Width = 800f, Height = 600f }; public readonly UiItemList ItemList = new(); public readonly UiScrollbar ItemScrollbar = new(); public readonly UiMenu TypeMenu = new(); public readonly UiText ItemNameText = new(); public readonly UiText ItemCostText = new(); public readonly UiText ItemsTab = new(); public readonly UiText BuyingTab = new(); public readonly UiText SellingTab = new(); public readonly UiElement ItemsPage = new TestElement(); public readonly UiElement BuyingPage = new TestElement(); public readonly UiElement SellingPage = new TestElement(); public readonly UiButton CloseButton; public readonly RetailWindowHandle Window; public readonly VendorUiController Controller; public Harness() { var root = new TestElement { Width = 800f, Height = 110f }; CloseButton = new UiButton( new ElementInfo { Id = VendorUiController.CloseId, Type = 1 }, static _ => (0u, 0, 0)); root.AddChild(CloseButton); root.AddChild(ItemsTab); root.AddChild(BuyingTab); root.AddChild(SellingTab); root.AddChild(ItemsPage); root.AddChild(BuyingPage); root.AddChild(SellingPage); ItemsPage.AddChild(ItemList); ItemsPage.AddChild(ItemScrollbar); ItemsPage.AddChild(TypeMenu); ItemsPage.AddChild(ItemNameText); ItemsPage.AddChild(ItemCostText); var layout = new ImportedLayout(root, new Dictionary { [VendorUiController.CloseId] = CloseButton, [VendorUiController.ItemsTabId] = ItemsTab, [VendorUiController.BuyingTabId] = BuyingTab, [VendorUiController.SellingTabId] = SellingTab, [VendorUiController.ItemsPageId] = ItemsPage, [VendorUiController.BuyingPageId] = BuyingPage, [VendorUiController.SellingPageId] = SellingPage, [VendorUiController.ItemListId] = ItemList, [VendorUiController.ItemScrollbarId] = ItemScrollbar, [VendorUiController.TypeFilterMenuId] = TypeMenu, [VendorUiController.ItemNameTextId] = ItemNameText, [VendorUiController.ItemCostTextId] = ItemCostText, }); Window = RetailWindowFrame.Mount( Screen, root, static _ => (0u, 0, 0), new RetailWindowFrame.Options { WindowName = WindowNames.Vendor, Chrome = RetailWindowChrome.Imported, Visible = false, Draggable = false, Resizable = false, }); Controller = VendorUiController.Bind( layout, State, Window, static (_, iconId, _, _, _) => iconId != 0u ? iconId + 1000u : 0u)!; Screen.WindowManager.AttachController(WindowNames.Vendor, Controller); } } private static VendorShopProfile Profile( float sellRate = 1.5f, uint altCurrency = 0u, string altName = "") => new(0u, 0u, 0u, false, 1.0f, sellRate, altCurrency, 0u, altName); private static string GetText(UiText text) => string.Concat(text.LinesProvider().Select(line => line.Text)); [Fact] public void Bind_WiresTheScrollbarToTheItemListsScrollModel() { var h = new Harness(); Assert.True(h.ItemScrollbar.Horizontal); Assert.Same(h.ItemList.Scroll, h.ItemScrollbar.Model); } [Fact] public void Opened_ShowsWindowAndPopulatesFirstPresentCategoryInTableOrder() { var h = new Harness(); var items = new[] { // Food is table entry #5, Armor is #1 — retail's fixed table // order, not insertion order, decides the default selection. new VendorShopItem(FoodItemGuid, -1, 1u, "Bread", (uint)ItemType.Food, 100u, 5), new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500), }; h.State.Apply(VendorGuid, Profile(), items); Assert.True(h.Window.IsVisible); Assert.True(h.ItemsPage.Visible); Assert.False(h.BuyingPage.Visible); Assert.False(h.SellingPage.Visible); Assert.Equal(1, h.ItemList.GetNumUIItems()); Assert.Equal(ArmorItemGuid, h.ItemList.GetItem(0)!.ItemId); Assert.Equal("Armor", h.TypeMenu.Items.Single(i => Equals(i.Payload, h.TypeMenu.Selected)).Label); } [Fact] public void Closed_HidesWindowAndClearsListAndText() { var h = new Harness(); h.State.Apply(VendorGuid, Profile(), new[] { new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500), }); h.ItemList.GetItem(0)!.Clicked?.Invoke(); Assert.True(h.Window.IsVisible); Assert.NotEqual(string.Empty, GetText(h.ItemNameText)); h.State.Close(); Assert.False(h.Window.IsVisible); Assert.Equal(0, h.ItemList.GetNumUIItems()); Assert.Equal(string.Empty, GetText(h.ItemNameText)); Assert.Equal(string.Empty, GetText(h.ItemCostText)); Assert.Empty(h.TypeMenu.Items); } [Fact] public void Reset_HidesWindowAndClearsContent() { var h = new Harness(); h.State.Apply(VendorGuid, Profile(), new[] { new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500), }); h.State.Reset(); Assert.False(h.Window.IsVisible); Assert.Equal(0, h.ItemList.GetNumUIItems()); } [Fact] public void SelectingItem_ShowsRetailCorrectPerUnitPrice_ForStackedItem() { var h = new Harness(); h.State.Apply(VendorGuid, Profile(sellRate: 2.0f), new[] { // Value=1000 is the STACK-TOTAL wire value for a 100-unit stack; // per-unit = 1000/100 = 10. SellPrice = ceil(2.0*10*1 - 0.1) = 20 // (VendorPricing.SellPrice, ShopSystem::SellPrice pc:702107). new VendorShopItem( StackedItemGuid, -1, 3u, "Arrows", (uint)ItemType.MissileWeapon, 300u, 1000, DescStackSize: 100), }); h.ItemList.GetItem(0)!.Clicked?.Invoke(); Assert.Equal("Arrows", GetText(h.ItemNameText)); Assert.Equal("20", GetText(h.ItemCostText)); } [Fact] public void SelectingItem_WithAlternateCurrency_AppendsCurrencyName() { var h = new Harness(); h.State.Apply( VendorGuid, Profile(sellRate: 1.0f, altCurrency: 0x12345678u, altName: "Trade Notes"), new[] { new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 50), }); h.ItemList.GetItem(0)!.Clicked?.Invoke(); Assert.Equal("50 Trade Notes", GetText(h.ItemCostText)); } [Fact] public void SelectingDifferentCategory_FiltersItemListToThatCategoryOnly() { var h = new Harness(); var armor = new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500); var food = new VendorShopItem(FoodItemGuid, -1, 1u, "Bread", (uint)ItemType.Food, 100u, 5); h.State.Apply(VendorGuid, Profile(), new[] { armor, food }); Assert.Equal(ArmorItemGuid, h.ItemList.GetItem(0)!.ItemId); // default: Armor (table order) object? foodPayload = h.TypeMenu.Items.First(i => i.Label == "Food").Payload; h.TypeMenu.OnSelect!.Invoke(foodPayload); Assert.Equal(1, h.ItemList.GetNumUIItems()); Assert.Equal(FoodItemGuid, h.ItemList.GetItem(0)!.ItemId); } [Fact] public void CloseButton_RoutesThroughVendorStateClose_NotADirectFieldWrite() { var h = new Harness(); h.State.Apply(VendorGuid, Profile(), new[] { new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500), }); h.CloseButton.OnClick!.Invoke(); // The panel never mutates VendorState directly — the ONLY path from // the close button to a cleared session is VendorState.Close() // itself, so VendorId reads back 0 through the owner's own public // surface, not a private field poke. Assert.Equal(0u, h.State.VendorId); Assert.False(h.Window.IsVisible); } [Fact] public void SellingTab_SwitchesPageButPopulatesNoSellContent() { var h = new Harness(); h.State.Apply(VendorGuid, Profile(), new[] { new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500), }); h.SellingTab.OnClick!.Invoke(); Assert.True(h.SellingPage.Visible); Assert.False(h.ItemsPage.Visible); Assert.False(h.BuyingPage.Visible); // Slice 6 fence: no sell-list/price/button wiring exists at all — // the page is exactly the authored-empty container it started as. Assert.Empty(h.SellingPage.Children); } [Fact] public void BuyingTab_SwitchesPageButPopulatesNoBuyContent() { var h = new Harness(); h.State.Apply(VendorGuid, Profile(), new[] { new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500), }); h.BuyingTab.OnClick!.Invoke(); Assert.True(h.BuyingPage.Visible); Assert.False(h.ItemsPage.Visible); Assert.Empty(h.BuyingPage.Children); } [Fact] public void ItemsTab_ReselectedAfterVisitingOtherTabs_ShowsBrowseListAgain() { var h = new Harness(); h.State.Apply(VendorGuid, Profile(), new[] { new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500), }); h.SellingTab.OnClick!.Invoke(); h.ItemsTab.OnClick!.Invoke(); Assert.True(h.ItemsPage.Visible); Assert.False(h.SellingPage.Visible); Assert.Equal(1, h.ItemList.GetNumUIItems()); } }