diff --git a/tests/AcDream.Plugins.MossTank.Tests/MossTankMarkupContractTests.cs b/tests/AcDream.Plugins.MossTank.Tests/MossTankMarkupContractTests.cs
index 3f4e8899..85642363 100644
--- a/tests/AcDream.Plugins.MossTank.Tests/MossTankMarkupContractTests.cs
+++ b/tests/AcDream.Plugins.MossTank.Tests/MossTankMarkupContractTests.cs
@@ -453,48 +453,80 @@ public sealed class MossTankMarkupContractTests
///
/// Fix round A (2026-09-07): the Advanced Options and Loot Editor popups
/// moved out of mosstank.xml into their own plugin panel files. This
- /// pins that both files still parse, still declare VTank's own popup
- /// footprint (392x300 / 268x300, docs/research/vtank-kb/08-ui-views.md
+ /// pins that every secondary popup file still parses, still declares
+ /// VTank's own popup footprint (docs/research/vtank-kb/08-ui-views.md
/// §1's secondary-view table), every child fits its own declared bounds,
/// and every markup binding still resolves against a real
/// property — the same three guarantees
///
/// and
- /// give the main panel, now split across three files instead of one.
+ /// give the main panel, now split across multiple files instead of one.
+ /// Fix round C item F3: the file list used to be a hardcoded
+ /// [InlineData] set that drifted the moment a popup was added or
+ /// renamed without a matching row here. It is now discovered with the
+ /// exact same glob the two "covers a new popup for free" tests already
+ /// use (,
+ /// ),
+ /// excluding mosstank.xml itself (the main panel, covered by the two
+ /// tests cited above). A newly discovered popup with no entry in
+ /// fails loudly, naming the file,
+ /// instead of being silently skipped.
///
- [Theory]
- [InlineData("mosstank-advanced.xml", 392f, 476f)]
- [InlineData("mosstank-loot-editor.xml", 268f, 300f)]
- [InlineData("mosstank-buffpicker.xml", 268f, 236f)]
- [InlineData("mosstank-metaeditor.xml", 630f, 160f)]
- public void SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves(
- string fileName, float expectedWidth, float expectedHeight)
+ private static readonly Dictionary ExpectedPopupBounds =
+ new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["mosstank-advanced.xml"] = (392f, 476f),
+ ["mosstank-loot-editor.xml"] = (268f, 300f),
+ ["mosstank-buffpicker.xml"] = (268f, 236f),
+ ["mosstank-metaeditor.xml"] = (630f, 160f),
+ };
+
+ [Fact]
+ public void SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves()
{
- XDocument document = XDocument.Load(
- Path.Combine(AppContext.BaseDirectory, fileName));
- XElement root = Assert.IsType(document.Root);
-
- Assert.Equal(expectedWidth, Number(root, "w"));
- Assert.Equal(expectedHeight, Number(root, "h"));
- AssertWithinParent(root);
-
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[] popupFileNames = Directory.GetFiles(AppContext.BaseDirectory, "mosstank*.xml")
+ .Select(static path => Path.GetFileName(path)!)
+ .Where(static name => !string.Equals(
+ name, "mosstank.xml", StringComparison.OrdinalIgnoreCase))
+ .OrderBy(static name => name, StringComparer.Ordinal)
+ .ToArray();
+ Assert.NotEmpty(popupFileNames);
+
+ foreach (string fileName in popupFileNames)
{
- 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}> in "
- + $"{fileName} has no public MossTankPanel property.");
+ ExpectedPopupBounds.TryGetValue(fileName, out (float Width, float Height) expected),
+ $"{fileName} has no expected width/height entry in "
+ + $"{nameof(ExpectedPopupBounds)} — add one (VTank's own "
+ + "secondary-view footprint from docs/research/vtank-kb/"
+ + "08-ui-views.md §1) before this popup can be trusted.");
+
+ XDocument document = XDocument.Load(
+ Path.Combine(AppContext.BaseDirectory, fileName));
+ XElement root = Assert.IsType(document.Root);
+
+ Assert.Equal(expected.Width, Number(root, "w"));
+ Assert.Equal(expected.Height, Number(root, "h"));
+ AssertWithinParent(root);
+
+ 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}> in "
+ + $"{fileName} has no public MossTankPanel property.");
+ }
}
}