diff --git a/tests/AcDream.App.Tests/UI/Layout/JournalPanelButtonsAreClickableTests.cs b/tests/AcDream.App.Tests/UI/Layout/JournalPanelButtonsAreClickableTests.cs new file mode 100644 index 00000000..b040147b --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/JournalPanelButtonsAreClickableTests.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AcDream.App.UI; +using AcDream.App.UI.Layout; +using AcDream.Content; +using DatReaderWriter; +using DatReaderWriter.Options; + +namespace AcDream.App.Tests.UI.Layout; + +/// +/// The Journal panel's buttons must be clickable when built from the REAL +/// authored layout. +/// +/// +/// +/// Every button on this panel authors property 0x0D, which was read as +/// "starts disabled" — so all of them built visible and dead. The hermetic +/// UiButton tests could not catch it because they construct their own +/// ElementInfo; only the installed layout carries the property. +/// +/// +/// This asserts the two things the player actually experiences: the button is +/// enabled, and a click at its centre reaches IT rather than falling through to +/// whatever is behind. +/// +/// +[Trait("Lane", "InstalledDat")] +public sealed class JournalPanelButtonsAreClickableTests +{ + private static readonly (uint Id, string Name)[] Buttons = + [ + (0x100005DCu, "Abandon (contracts)"), + (0x10000567u, "New"), + (0x1000056Fu, "First"), + (0x10000571u, "Last"), + (0x10000565u, "Previous"), + (0x10000566u, "Next"), + (0x10000574u, "Record"), + (0x1000057Du, "Start"), + (0x10000585u, "Delete (page list)"), + (0x10000588u, "Reset (page list)"), + ]; + + 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; " + + "see docs/release-gate.md."); + } + + 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); + } + + [Fact] + public void EveryJournalPanelButtonBuildsEnabled() + { + ImportedLayout layout = BuildPanel(); + + var dead = new List(); + foreach ((uint id, string name) in Buttons) + { + if (layout.FindElement(id) is not UiButton button) + { + dead.Add($"{name} (0x{id:X8}) did not build as a UiButton"); + continue; + } + + if (!button.Enabled) + dead.Add($"{name} (0x{id:X8}) built DISABLED"); + } + + Assert.Empty(dead); + } + + [Fact] + public void EveryJournalPanelButtonIsReachableByAClickAtItsCentre() + { + // Enabled alone is not enough — the click has to actually land on it + // rather than pass through to something behind. + ImportedLayout layout = BuildPanel(); + + var unreachable = new List(); + foreach ((uint id, string name) in Buttons) + { + if (layout.FindElement(id) is not UiButton button) + continue; + + UiElement? hit = HitTestSelf(button); + if (!ReferenceEquals(hit, button)) + unreachable.Add($"{name} (0x{id:X8}) hit-tested to {hit?.GetType().Name ?? "nothing"}"); + } + + Assert.Empty(unreachable); + } + + /// Hit-tests a button at its own centre, in its own local space. + private static UiElement? HitTestSelf(UiButton button) + { + var hitTest = typeof(UiElement).GetMethod( + "HitTest", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!; + return hitTest.Invoke( + button, + [button.Width / 2f, button.Height / 2f]) as UiElement; + } +}