using System.Linq; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Net.Messages; using AcDream.Core.World; namespace AcDream.App.Tests.UI.Layout; /// /// Controller-level tests for against /// the committed map_house_2100006E_1000018C.json fixture (Batch C, /// overnight hover/UI round) — the SAME LayoutImporter.ImportInfos(dats, /// 0x2100006Eu, 0x1000018Cu) catalog import /// performs against real /// DATs. No DAT access, no live runtime — dat-free per the established /// pattern. /// public sealed class MapHousePanelControllerTests { /// Serves the town-hotspot template. Returns a /// — matching the live template's own authored /// Type 1 (MapHousePanelSlotProbeTests: "hotspot template /// type=1") — so 's /// marker is UiButton tooltip-text branch is actually exercised /// by these tests. A real would set /// DatElementId the same way /// does, so tests that need to find these markers back by id after the /// fact need it too. private static UiElement? FakeHotspotTemplate(uint layoutId, uint elementId) => new UiButton(new ElementInfo(), static _ => (0u, 0, 0)) { Width = 10f, Height = 10f, DatElementId = elementId, }; /// The /// seam: builds m_pMap's two button-swallowed icon children from their /// OWN s inside the panel-slot resolve tree /// (register row AD-108 — a standalone re-import cannot find them on /// the live DAT, so the icons are found under the already-resolved /// pageInfo and built through this seam instead). Mirrors /// production's LayoutImporter.Build(info, ...).Root, which sets /// DatElementId from the info's own id. private static UiElement? FakeIconBuilder(ElementInfo info) => new UiButton(new ElementInfo(), static _ => (0u, 0, 0)) { Width = 10f, Height = 10f, DatElementId = info.Id, }; /// The House ListBox's own row template resolves to a /// in the live DAT (MapHousePanelSlotProbeTests: /// "row template type=12" — UIElement_Text), unlike the Map tab's /// UiButton hotspots above — a fresh instance per call, matching /// production's real resolver. private static UiElement? FakeHouseRowTemplate(uint layoutId, uint elementId) => new UiText { Width = 280f, Height = 28f }; private static MapHousePanelController.Callbacks MakeCallbacks( List? calls = null, Func? currentCalendar = null, Func? playerCellId = null, Func? housePosition = null, Func>? houseLines = null) { calls ??= new List(); return new MapHousePanelController.Callbacks( Toggle: () => calls.Add("toggle"), Map: new MapPageController.Bindings( CurrentCalendar: currentCalendar ?? (static () => default), PlayerCellId: playerCellId ?? (static () => 0u), HousePosition: housePosition ?? (static () => null), TemplateResolver: FakeHotspotTemplate, IconBuilder: FakeIconBuilder), House: new HousePageController.Bindings( Lines: houseLines ?? (static () => Array.Empty()), OnShown: () => calls.Add("house-shown"), TemplateResolver: FakeHouseRowTemplate)); } [Fact] public void Bind_RootBuildsAsUiTabPanel() { ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, MakeCallbacks()); Assert.NotNull(controller); Assert.IsType(controller!.Root); } /// Pins the authored tab table exactly as read from the live /// DATs (MapHousePanelSlotProbeTests): two entries, Map is the /// sole default. [Fact] public void TabTable_MatchesLiveDatPairing_MapIsDefault() { ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); var tabs = Assert.IsType(layout.Root); Assert.Equal(2, tabs.Tabs.Count); Assert.Contains(tabs.Tabs, e => e.ButtonElementId == 0x100001F3u && e.PageElementId == 0x100001F6u && e.IsDefault); Assert.Contains(tabs.Tabs, e => e.ButtonElementId == 0x100001F4u && e.PageElementId == 0x100001F7u && !e.IsDefault); Assert.Single(tabs.Tabs, e => e.IsDefault); } [Fact] public void Bind_Succeeds_AndActivateTabs_SelectsMapByDefault() { ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, MakeCallbacks()); Assert.NotNull(controller); controller!.ActivateTabs(); Assert.Empty(controller.TabPanel.UnresolvedEntries); Assert.False(controller.IsShowingHouse); } [Fact] public void SwitchToHouse_FiresOnShown_OnlyWhenPanelIsVisible() { ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); var calls = new List(); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, MakeCallbacks(calls)); Assert.NotNull(controller); controller!.ActivateTabs(); // Not visible yet: switching tabs must not fire OnShown. controller.TabPanel.SwitchTo(0x100001F7u); Assert.DoesNotContain("house-shown", calls); controller.OnShown(); Assert.Contains("house-shown", calls); } [Fact] public void CloseButton_InvokesToggle() { ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); var calls = new List(); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, MakeCallbacks(calls)); Assert.NotNull(controller); UiElement? close = layout.FindElement(0x100001F5u); Assert.IsType(close); ((UiButton)close!).OnClick?.Invoke(); Assert.Contains("toggle", calls); } [Fact] public void Bind_BuildsAll53TownHotspots_UnderTheMapWidget() { ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, MakeCallbacks()); Assert.NotNull(controller); UiElement? map = UiElement.FindDescendant(controller!.Root, MapPageController.MapWidgetId); Assert.NotNull(map); // m_pMap's own children are the player/house icons (found under the // panel-slot resolve tree and rebuilt via Bindings.IconBuilder — see // MapPageController.Bind's doc on why m_pMap being a Button swallows // its authored nested children) PLUS the 53 town hotspots. var townMarkers = map!.Children .Where(c => c.DatElementId != MapPageController.PlayerIconId && c.DatElementId != MapPageController.HouseIconId) .ToList(); Assert.Equal(55, map.Children.Count); Assert.Equal(53, townMarkers.Count); Assert.All(townMarkers, c => Assert.Contains( MapLocations.All, loc => loc.Width == c.Width && loc.Height == c.Height)); // Runtime tooltip text (UiButton.TooltipText, backing // GetTooltipText()) — the retail SetTooltip/m_TTText mechanism, NOT // the DAT-authored AuthoredTooltipText path. The popup-skin locator // is unconditionally required even on the runtime-text path (see // RetailTooltipPresenter.SharedPopupSkinRootElementId's doc). Assert.All(townMarkers, c => Assert.NotEqual(0u, c.AuthoredTooltipRootElementId)); Assert.All(townMarkers, c => Assert.NotEqual(0u, c.AuthoredTooltipLayoutDid)); Assert.All(townMarkers, c => Assert.IsType(c)); Assert.All(townMarkers, c => Assert.False(string.IsNullOrEmpty(((UiButton)c).TooltipText))); Assert.Contains(townMarkers, c => ((UiButton)c).TooltipText == "Holtburg"); } [Fact] public void Bind_HouseListBoxStartsEmpty_MatchingRetailPostInit() { ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); MapHousePanelController? controller = MapHousePanelController.Bind(rootInfo, layout, MakeCallbacks()); Assert.NotNull(controller); UiElement? box = UiElement.FindDescendant(controller!.Root, HousePageController.TextBoxId); var listBox = Assert.IsType(box); Assert.Equal(0, listBox.ContentHeight); } /// /// Batch C House-ownership-text closer (2026-08-17): the ONE /// decomp-verified gmHouseUI::DisplayPurchaseTimeText @0x004a3110 /// line a houseless character's HouseQuery response renders — proves /// 's revision-gated /// poll actually rebuilds the /// authored row template with real text end-to-end, the same way /// proves the text composition in /// isolation. /// [Fact] public void Tick_RendersHouseLinesIntoTheAuthoredRowTemplate() { ElementInfo rootInfo = FixtureLoader.LoadMapHouseHostInfos(); ImportedLayout layout = FixtureLoader.LoadMapHouseHost(); string[] lines = ["You may buy another house immediately."]; MapHousePanelController? controller = MapHousePanelController.Bind( rootInfo, layout, MakeCallbacks(houseLines: () => lines)); Assert.NotNull(controller); controller!.Tick(0.016); UiElement? box = UiElement.FindDescendant(controller.Root, HousePageController.TextBoxId); var listBox = Assert.IsType(box); // Rows land in the ListBox's internal scrollable viewport (AddChild // there, not directly on the ListBox itself — UiTemplateListBox's // own #372/#412 dormancy machinery), exposed to tests via // ViewportForTest. UiScrollablePanel viewport = Assert.IsType( listBox.ViewportForTest); Assert.Single(viewport.Children); var row = Assert.IsType(viewport.Children[0]); Assert.Equal( "You may buy another house immediately.", Assert.Single(row.LinesProvider()).Text); } }