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."); + } } }