Main window: Main, Settings, Statistics and Tools tabs as a markup panel

The original's notebook without the parts OpenAC or another plugin now owns
(updater, VTank path, route visualisation, chest looter). Labels read live
from the trackers each frame. Settings gains the backend URL and shared-secret
fields, since the secret is no longer compiled in. Tools opens the Mossy
Tracker and Metas windows through openers their features set. The WebSocket
label now shows the real connection state rather than the setting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-25 12:19:01 +02:00
parent 1ff0a40aeb
commit 5799ea956e
5 changed files with 379 additions and 0 deletions

View file

@ -0,0 +1,72 @@
using System.Reflection;
using System.Xml.Linq;
using OpenAC.MosswartMassacre.Views;
namespace OpenAC.MosswartMassacre.Tests.Views;
/// <summary>
/// Every <c>{Binding}</c> in a markup file names a public property of the
/// shape the host expects, so a typo fails here instead of as a panel that
/// refuses to build in the client.
/// </summary>
public sealed class MarkupContractTests
{
public static TheoryData<string, Type> MarkupFiles => new()
{
{ MainWindow.MarkupFile, typeof(MainWindow) },
};
[Theory]
[MemberData(nameof(MarkupFiles))]
public void EveryBindingResolvesWithTheRightShape(string file, Type bindingType)
{
XElement root = XDocument.Load(MarkupPath(file)).Root!;
Dictionary<string, PropertyInfo> properties = bindingType
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(property => property.Name, StringComparer.Ordinal);
foreach (XAttribute attribute in root.DescendantsAndSelf().Attributes())
{
string value = attribute.Value;
if (!value.StartsWith('{'))
continue;
Assert.Matches("^\\{[^{}]+\\}$", value);
string name = value[1..^1];
Assert.True(properties.TryGetValue(name, out PropertyInfo? property),
$"{file}: {value} on <{attribute.Parent!.Name}> names no public property.");
string element = attribute.Parent!.Name.LocalName;
Type expected = (element, attribute.Name.LocalName) switch
{
(_, "onclick") => typeof(Action),
("field", "onchange" or "onsubmit") => typeof(Action<string>),
("menu", "onchange") => typeof(Action<string>),
("list", "onchange") => typeof(Action<int>),
("slider", "onchange") => typeof(Action<float>),
(_, "checked" or "selected" or "visible" or "enabled") when element != "list" && element != "menu" => typeof(bool),
_ => typeof(object),
};
if (expected != typeof(object))
Assert.True(property!.PropertyType == expected,
$"{file}: {value} on <{element} {attribute.Name}> is {property.PropertyType.Name}, expected {expected.Name}.");
}
}
[Fact]
public void The_main_window_builds_its_bindings_without_a_host_ui()
{
using var harness = new PluginHarness();
harness.Login();
MainWindow window = harness.Feature<MainWindow>();
Assert.Equal("Total Kills: 0", window.TotalKillsText);
Assert.Equal("Meta State: Unknown", window.MetaStateText);
Assert.Equal("WebSocket: [DISCONNECTED]", window.WebSocketText);
Assert.Equal("Never", window.LastKillValue);
window.ShowStatistics();
Assert.True(window.StatisticsSelected);
Assert.False(window.MainSelected);
}
private static string MarkupPath(string file) => Path.Combine(AppContext.BaseDirectory, file);
}