using System; using System.Collections.Generic; using System.IO; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Content; using AcDream.Core.Journal; using AcDream.Runtime.Gameplay; using DatReaderWriter; using DatReaderWriter.Options; namespace AcDream.App.Tests.UI.Layout; /// /// The controllers must actually WIRE the real panel. /// /// /// Everything else so far tested the layout (do the widgets build right?) or /// the controller against a hand-built page (does the logic work?). Neither /// proves the controller finds its elements in the REAL tree — which is the /// one step where an id typo or a subtree assumption silently produces a panel /// where nothing responds. /// [Trait("Lane", "InstalledDat")] public sealed class JournalPanelLiveBindTests { private const uint OutdoorCell = 0xA9B4001Fu; private static ImportedLayout BuildPanel() { string? datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); if (string.IsNullOrWhiteSpace(datDir) || !Directory.Exists(datDir)) { datDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); } if (!Directory.Exists(datDir)) Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory."); using var dats = new DatCollection(datDir, DatAccessType.Read); using var adapter = new DatCollectionAdapter(dats); ElementInfo? root = LayoutImporter.ImportInfos( adapter, JournalPanelController.HostLayoutId, JournalPanelController.SlotElementId); Assert.NotNull(root); var strings = new DatStringResolver(adapter); return LayoutImporter.Build(root!, _ => (0u, 0, 0), null, _ => null, strings.Resolve); } private static (JournalNotesPageController Controller, RuntimeJournalState State, UiElement Page) BindNotes(ImportedLayout layout, Func? playerCell = null) { UiElement? page = layout.FindElement(JournalPanelController.NotesPageId); Assert.NotNull(page); var state = new RuntimeJournalState(); state.Load([new JournalPage()]); var controller = new JournalNotesPageController( page!, new JournalNotesPageController.Bindings( Journal: state.View, Commands: state, PlayerCell: playerCell ?? (() => OutdoorCell), Now: () => new DateTime(2026, 8, 21, 12, 0, 0, DateTimeKind.Utc))); return (controller, state, page!); } [Fact] public void TheNotesPageWiresEveryButtonItOwns() { // If FindDescendant misses one — wrong id, or the element is not in the // subtree the controller searches — that button silently does nothing // and nothing else fails. ImportedLayout layout = BuildPanel(); (_, RuntimeJournalState state, UiElement page) = BindNotes(layout); var unwired = new List(); foreach ((uint id, string name) in new[] { (0x10000565u, "Previous"), (0x10000566u, "Next"), (0x10000567u, "New"), (0x1000056Fu, "First"), (0x10000571u, "Last"), (0x10000574u, "Record"), (0x1000057Du, "Start"), }) { if (UiElement.FindDescendant(page, id) is not UiButton button) unwired.Add($"{name} (0x{id:X8}) not found under the notes page"); else if (button.OnClick is null) unwired.Add($"{name} (0x{id:X8}) has no OnClick"); } Assert.Empty(unwired); state.Dispose(); } [Fact] public void RecordOnTheRealPageStampsAndDisplaysTheLocation() { ImportedLayout layout = BuildPanel(); (_, RuntimeJournalState state, UiElement page) = BindNotes(layout); (UiElement.FindDescendant(page, 0x10000574u) as UiButton)!.OnClick!(); Assert.True(state.View.Current.HasLocation); var readout = UiElement.FindDescendant(page, 0x10000573u) as UiField; Assert.NotNull(readout); Assert.NotEqual(string.Empty, readout!.Text); state.Dispose(); } [Fact] public void RecordIndoorsDoesNothingBecauseThereAreNoCoordinates() { // retail's own gid_to_lcoord failure. Worth pinning because it looks // identical to a broken button from the player's side. ImportedLayout layout = BuildPanel(); (_, RuntimeJournalState state, UiElement page) = BindNotes(layout, playerCell: () => 0x01020304u); (UiElement.FindDescendant(page, 0x10000574u) as UiButton)!.OnClick!(); Assert.False(state.View.Current.HasLocation); state.Dispose(); } [Fact] public void StartOnTheRealPageSwapsToTheRunningReadout() { ImportedLayout layout = BuildPanel(); (_, RuntimeJournalState state, UiElement page) = BindNotes(layout); var hours = UiElement.FindDescendant(page, 0x10000578u) as UiField; Assert.NotNull(hours); hours!.SetText("1"); (UiElement.FindDescendant(page, 0x1000057Du) as UiButton)!.OnClick!(); Assert.False(hours.Visible); UiElement? readout = UiElement.FindDescendant(page, 0x1000057Cu); Assert.NotNull(readout); Assert.True(readout!.Visible); state.Dispose(); } [Fact] public void ThePageListWiresItsButtonsToo() { ImportedLayout layout = BuildPanel(); UiElement? page = layout.FindElement(JournalPanelController.PageListPageId); Assert.NotNull(page); var state = new RuntimeJournalState(); state.Load([new JournalPage(Title: "one")]); _ = new JournalPageListController( page!, new JournalPageListController.Bindings( Journal: state.View, Commands: state, OpenPage: _ => { }, TemplateResolver: (_, _) => null)); var unwired = new List(); foreach ((uint id, string name) in new[] { (0x10000585u, "Delete"), (0x10000588u, "Reset"), }) { if (UiElement.FindDescendant(page!, id) is not UiButton button) unwired.Add($"{name} (0x{id:X8}) not found"); else if (button.OnClick is null) unwired.Add($"{name} (0x{id:X8}) has no OnClick"); } Assert.Empty(unwired); state.Dispose(); } }