using System.Reflection; using System.Xml.Linq; using AcDream.Plugin.Abstractions; namespace AcDream.Plugins.MossTank.Tests; public sealed class MossTankMarkupContractTests { [Fact] public void VtankTabOrderAndEveryBindingResolveAgainstTheLivePanel() { XDocument document = XDocument.Load( Path.Combine(AppContext.BaseDirectory, "mosstank.xml")); XElement root = Assert.IsType(document.Root); Assert.Equal( [ "Options", "Profiles", "Vitals", "Monsters", "Items", "Consumables", "Buffs", "Route", "Meta", ], root.Elements("tab") .Select(static tab => (string?)tab.Attribute("text"))); PropertyInfo[] properties = typeof(MossTankPanel).GetProperties( BindingFlags.Instance | BindingFlags.Public); var byName = properties.ToDictionary( static property => property.Name, StringComparer.Ordinal); foreach (XAttribute attribute in root.DescendantsAndSelf().Attributes()) { string value = attribute.Value; if (!value.Contains('{', StringComparison.Ordinal)) continue; Assert.Matches("^\\{[^{}]+\\}$", value); string name = value[1..^1]; Assert.True( byName.ContainsKey(name), $"Markup binding {value} on <{attribute.Parent?.Name}> has no " + $"public MossTankPanel property."); } } [Fact] public void EveryInteractiveBindingMatchesTheRetainedUiDelegateShape() { XDocument document = XDocument.Load( Path.Combine(AppContext.BaseDirectory, "mosstank.xml")); XElement root = Assert.IsType(document.Root); PropertyInfo[] properties = typeof(MossTankPanel).GetProperties( BindingFlags.Instance | BindingFlags.Public); var byName = properties.ToDictionary( static property => property.Name, StringComparer.Ordinal); foreach (XElement element in root.DescendantsAndSelf()) { AssertBindingType(element, "onclick", typeof(Action), byName); AssertBindingType( element, "onsubmit", typeof(Action), byName); Type? changeType = element.Name.LocalName switch { "field" or "menu" => typeof(Action), "slider" => typeof(Action), "list" => typeof(Action), _ => null, }; if (changeType is not null) AssertBindingType(element, "onchange", changeType, byName); } } [Fact] public void EveryInteractiveControlDeclaresARealHandlerBinding() { XDocument document = XDocument.Load( Path.Combine(AppContext.BaseDirectory, "mosstank.xml")); XElement root = Assert.IsType(document.Root); HashSet interactive = new( [ "tab", "button", "toggle", "slider", "field", "menu", "list", ], StringComparer.Ordinal); XElement[] controls = root.Descendants() .Where(element => interactive.Contains(element.Name.LocalName)) .ToArray(); Assert.Equal(190, controls.Length); foreach (XElement control in controls) { XAttribute? handler = control.Attribute("onclick") ?? control.Attribute("onchange") ?? control.Attribute("onsubmit"); Assert.NotNull(handler); Assert.NotEqual("false", (string?)control.Attribute("enabled")); } } [Fact] public void EveryVtankTabIsBackedByALivePanelSurface() { var panel = new MossTankPanel(new StubHost()); Assert.True(panel.OptionsTabEnabled); Assert.True(panel.VitalsTabEnabled); Assert.True(panel.MonstersTabEnabled); Assert.True(panel.BuffsTabEnabled); Assert.True(panel.ProfilesTabEnabled); Assert.True(panel.ItemsTabEnabled); Assert.True(panel.ConsumablesTabEnabled); Assert.True(panel.RouteTabEnabled); Assert.True(panel.MetaTabEnabled); } [Fact] public void AuthoredShellFitsTheMinimumCanvasAndEverySizedChildFitsItsParent() { XDocument document = XDocument.Load( Path.Combine(AppContext.BaseDirectory, "mosstank.xml")); XElement root = Assert.IsType(document.Root); Assert.Equal(800f, Number(root, "w")); Assert.Equal(244f, Number(root, "h")); AssertWithinParent(root); } [Fact] public void TextlessAndAbbreviatedControlsHaveAccessibleRetailTooltips() { XDocument document = XDocument.Load( Path.Combine(AppContext.BaseDirectory, "mosstank.xml")); XElement root = Assert.IsType(document.Root); string[] interactive = [ "tab", "button", "toggle", "slider", "field", "menu", "list", ]; HashSet terse = new( [ "+", "-", "↑", "↓", "F", "B", "G", "I", "Y", "V", "A", "R", "S", "W", "FC", "Cp", "DC", "Cs", ], StringComparer.Ordinal); foreach (XElement element in root.Descendants() .Where(element => interactive.Contains( element.Name.LocalName, StringComparer.Ordinal))) { string? text = (string?)element.Attribute("text"); if (!string.IsNullOrWhiteSpace(text) && !terse.Contains(text)) continue; Assert.False( string.IsNullOrWhiteSpace((string?)element.Attribute("tooltip")), $"<{element.Name}> text='{text}' needs a tooltip."); } } private static void AssertBindingType( XElement element, string attributeName, Type expectedType, IReadOnlyDictionary properties) { string? expression = (string?)element.Attribute(attributeName); if (expression is null) return; Assert.StartsWith("{", expression, StringComparison.Ordinal); Assert.EndsWith("}", expression, StringComparison.Ordinal); string name = expression[1..^1]; Assert.True( properties.TryGetValue(name, out PropertyInfo? property), $"Markup binding {expression} on <{element.Name}> has no public " + "MossTankPanel property."); Assert.Equal(expectedType, property.PropertyType); } private static void AssertWithinParent(XElement parent) { float parentWidth = Number(parent, "w"); float parentHeight = Number(parent, "h"); foreach (XElement child in parent.Elements()) { float width = Number(child, "w"); float height = Number(child, "h"); if (width > 0f) { Assert.True( Number(child, "x") + width <= parentWidth, $"<{child.Name}> crosses the right edge of <{parent.Name}>."); } if (height > 0f) { Assert.True( Number(child, "y") + height <= parentHeight, $"<{child.Name}> crosses the bottom edge of <{parent.Name}>."); } AssertWithinParent(child); } } private static float Number(XElement element, string attribute) => float.TryParse( (string?)element.Attribute(attribute), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float value) ? value : 0f; private sealed class StubHost : IPluginHost { public bool HasUi => false; public IPluginLogger Log { get; } = new StubLogger(); public IGameState State { get; } = new StubState(); public IEvents Events { get; } = new StubEvents(); public ISelectionService Selection { get; } = new StubSelection(); public IUiRegistry Ui => NoOpUiRegistry.Instance; public IAutomationSurface Automation => NoOpAutomationSurface.Instance; } private sealed class StubLogger : IPluginLogger { public void Info(string message) { } public void Warn(string message) { } public void Error(string message, Exception? exception = null) { } } private sealed class StubState : IGameState { public IReadOnlyList Entities => []; } private sealed class StubEvents : IEvents { public event Action EntitySpawned { add { } remove { } } public event Action Tick { add { } remove { } } } private sealed class StubSelection : ISelectionService { public uint? SelectedObjectId => null; public uint? PreviousObjectId => null; public event Action Changed { add { } remove { } } public bool Select(uint objectId) => false; public bool Clear() => false; } }