The owner calls the flag tracker important, so the original's pages come across with their tables unchanged: augmentations and luminance auras from the player's own int properties (the object table carries them), recalls from the spellbook with composited spell icons, cantrips matched from the active enchantments to trained skills, attributes and wards, the tracked weapons the original kept data for but never showed, and the quest timers. Read on the tick thread at login and on each page's Refresh; the quest page redraws every five seconds instead of from a thread-pool timer. The root panel's visibility is the settable IsVisible so the main window can open it; the three sections the original left as stubs stay out, and a headless host registers nothing. The fake spell catalog learns IsKnown. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
220 lines
8.6 KiB
C#
220 lines
8.6 KiB
C#
using System.Reflection;
|
|
using System.Xml.Linq;
|
|
using OpenAC.MosswartMassacre.FlagTracker;
|
|
|
|
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) },
|
|
};
|
|
|
|
[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);
|
|
}
|