using System.Reflection; using System.Xml.Linq; using OpenAC.MosswartMassacre.FlagTracker; namespace OpenAC.MosswartMassacre.Tests.Ui; /// /// Proves every {Binding} in this slice's markup files resolves to a /// public property of the right shape on its binding object, the check the /// host would otherwise only make when the window is built in game. /// public sealed class TrackerAndMetasMarkupContractTests { private static readonly string[] InteractiveElementNames = [ "tab", "button", "toggle", "slider", "field", "menu", "list", "column", ]; public static TheoryData MarkupFiles => new() { { FlagTrackerFeature.MarkupFile, typeof(FlagTrackerViewModel) }, }; [Theory] [MemberData(nameof(MarkupFiles))] public void EveryBindingResolvesToAPublicProperty(string file, Type bindingType) { XElement root = LoadRoot(file); IReadOnlyDictionary byName = PropertiesOf(bindingType); 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), $"{file}: binding {value} on <{attribute.Parent?.Name}> has no public {bindingType.Name} property."); } } [Theory] [MemberData(nameof(MarkupFiles))] public void EveryBindingHasTheShapeTheHostReads(string file, Type bindingType) { XElement root = LoadRoot(file); IReadOnlyDictionary byName = PropertiesOf(bindingType); foreach (XElement element in root.DescendantsAndSelf()) AssertElementBindings(file, element, byName); } [Theory] [MemberData(nameof(MarkupFiles))] public void EveryInteractiveControlDeclaresAHandler(string file, Type bindingType) { _ = bindingType; XElement root = LoadRoot(file); HashSet interactive = new(InteractiveElementNames, StringComparer.Ordinal); XElement[] controls = [.. root.Descendants().Where(element => interactive.Contains(element.Name.LocalName))]; Assert.NotEmpty(controls); foreach (XElement control in controls) { if (control.Name.LocalName == "column" && (string?)control.Attribute("type") == "text") continue; XAttribute? handler = control.Attribute("onclick") ?? control.Attribute("onchange") ?? control.Attribute("onsubmit"); Assert.True(handler is not null, $"{file}: <{control.Name.LocalName}> declares no handler."); } } [Theory] [MemberData(nameof(MarkupFiles))] public void TheRootPanelVisibilityIsTheSettableIsVisible(string file, Type bindingType) { XElement root = LoadRoot(file); Assert.Equal("panel", root.Name.LocalName); Assert.Equal("{IsVisible}", (string?)root.Attribute("visible")); PropertyInfo isVisible = PropertiesOf(bindingType)["IsVisible"]; Assert.Equal(typeof(bool), isVisible.PropertyType); Assert.True(isVisible.CanWrite && isVisible.SetMethod!.IsPublic); } [Theory] [MemberData(nameof(MarkupFiles))] public void EveryListColumnHasASaneWidth(string file, Type bindingType) { _ = bindingType; XElement root = LoadRoot(file); foreach (XElement list in root.Descendants("list")) { XElement[] columns = [.. list.Elements("column")]; for (int index = 0; index < columns.Length; index++) { string? width = (string?)columns[index].Attribute("width"); Assert.True(width is not null, $"{file}: {index} has no width."); bool isStar = width == "*"; bool isPositive = double.TryParse( width, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double parsed) && parsed > 0d; Assert.True(isStar || isPositive, $"{file}: at {index}."); } } } [Fact] public void TheFlagTrackerTabsAreTheOriginalPagesPlusWeapons() { XElement root = LoadRoot(FlagTrackerFeature.MarkupFile); string[] tabs = [.. root.Elements("tab").Select(tab => (string?)tab.Attribute("text") ?? string.Empty)]; Assert.Equal(["Augs", "Lum", "Recalls", "Cantrips", "Weapons", "Quests"], tabs); Assert.Equal(6, root.Elements("group").Count()); Assert.Equal("Mossy Tracker", (string?)root.Attribute("title")); } private static void AssertElementBindings(string file, XElement element, IReadOnlyDictionary byName) { if (element.Name.LocalName == "column") { AssertBindingType(file, element, "onchange", typeof(Action), byName); AssertBindingType(file, element, "onclick", typeof(Action), byName); AssertListBinding(file, element, "items", typeof(string), byName); AssertListBinding(file, element, "colors", typeof(uint), byName); switch ((string?)element.Attribute("type")) { case "check": AssertListBinding(file, element, "values", typeof(bool), byName); break; case "icon": AssertListBinding(file, element, "values", typeof(uint), byName); break; } return; } AssertBindingType(file, element, "onclick", typeof(Action), byName); AssertBindingType(file, element, "onsubmit", typeof(Action), byName); AssertBindingType(file, element, "visible", typeof(bool), byName); AssertBindingType(file, element, "enabled", typeof(bool), byName); AssertBindingType(file, element, "checked", typeof(bool), byName); Type? selectedType = element.Name.LocalName switch { "tab" => typeof(bool), "list" => typeof(int), "menu" => null, _ => null, }; AssertBindingType(file, element, "selected", selectedType, byName); Type? changeType = element.Name.LocalName switch { "field" or "menu" => typeof(Action), "slider" => typeof(Action), "list" => typeof(Action), _ => null, }; AssertBindingType(file, element, "onchange", changeType, byName); if (element.Name.LocalName is "list" or "menu") AssertListBinding(file, element, "items", typeof(string), byName); } private static void AssertBindingType( string file, XElement element, string attributeName, Type? expected, IReadOnlyDictionary byName) { if (expected is null) return; string? value = (string?)element.Attribute(attributeName); if (value is null || !value.StartsWith('{')) return; PropertyInfo property = byName[value[1..^1]]; Assert.True( expected.IsAssignableFrom(property.PropertyType), $"{file}: <{element.Name.LocalName} {attributeName}=\"{value}\"> is {property.PropertyType.Name}, expected {expected.Name}."); } private static void AssertListBinding( string file, XElement element, string attributeName, Type elementType, IReadOnlyDictionary byName) { string? value = (string?)element.Attribute(attributeName); if (value is null || !value.StartsWith('{')) return; Type type = byName[value[1..^1]].PropertyType; Assert.True( typeof(IReadOnlyList<>).MakeGenericType(elementType).IsAssignableFrom(type), $"{file}: <{element.Name.LocalName} {attributeName}=\"{value}\"> is {type.Name}, expected IReadOnlyList<{elementType.Name}>."); } private static XElement LoadRoot(string file) { XDocument document = XDocument.Load(Path.Combine(AppContext.BaseDirectory, file)); return Assert.IsType(document.Root); } private static IReadOnlyDictionary PropertiesOf(Type type) => type.GetProperties(BindingFlags.Instance | BindingFlags.Public) .ToDictionary(static property => property.Name, StringComparer.Ordinal); }