openac-mosswartmassacre/tests/OpenAC.MosswartMassacre.Tests/Ui/TrackerAndMetasMarkupContractTests.cs
Erik 162d242c06 Meta sync: pull the club's metas and navs into the shared profiles
The original kept VTank's profile folder in step with the club's metas
repository; the port does the same against host.VtankProfiles, dropping
.met files under mosstank/metas and .nav files under mosstank/navs where
MossTank reads dropped profiles, flattened to their bare names as the
original flattened them. The Gitea tree and raw endpoints are the
original's; the repository address is the metas_repository_url setting,
defaulting to the original repository. An overwritten file keeps its old
copy as .bak (MossTank's pickers skip .bak side files).

Storage is text-only, so the SHA-256 compare hashes the text a stored read
gives back (UTF-8, BOM dropped); for the plain-text files in the repo that
equals the original's byte compare. Network work runs on the thread pool,
one operation after another; chat lines and window updates are applied on
the tick. The four /mm verbs keep their chat lines, and the Metas tab is
its own markup window with IsVisible for the main window to open.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:26:14 +02:00

222 lines
8.7 KiB
C#

using System.Reflection;
using System.Xml.Linq;
using OpenAC.MosswartMassacre.FlagTracker;
using OpenAC.MosswartMassacre.Metas;
namespace OpenAC.MosswartMassacre.Tests.Ui;
/// <summary>
/// Proves every <c>{Binding}</c> 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.
/// </summary>
public sealed class TrackerAndMetasMarkupContractTests
{
private static readonly string[] InteractiveElementNames =
[
"tab", "button", "toggle", "slider", "field", "menu", "list", "column",
];
public static TheoryData<string, Type> MarkupFiles => new()
{
{ FlagTrackerFeature.MarkupFile, typeof(FlagTrackerViewModel) },
{ MetaSyncFeature.MarkupFile, typeof(MetasViewModel) },
};
[Theory]
[MemberData(nameof(MarkupFiles))]
public void EveryBindingResolvesToAPublicProperty(string file, Type bindingType)
{
XElement root = LoadRoot(file);
IReadOnlyDictionary<string, PropertyInfo> 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<string, PropertyInfo> 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<string> 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}: <column> {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}: <column width=\"{width}\"> 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<string, PropertyInfo> byName)
{
if (element.Name.LocalName == "column")
{
AssertBindingType(file, element, "onchange", typeof(Action<int>), byName);
AssertBindingType(file, element, "onclick", typeof(Action<int>), 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<string>), 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<string>),
"slider" => typeof(Action<float>),
"list" => typeof(Action<int>),
_ => 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<string, PropertyInfo> 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<string, PropertyInfo> 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<XElement>(document.Root);
}
private static IReadOnlyDictionary<string, PropertyInfo> PropertiesOf(Type type) =>
type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(static property => property.Name, StringComparer.Ordinal);
}