using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.UI; using AcDream.Plugin.Abstractions; using AcDream.Plugins.MossTank; using Xunit; namespace AcDream.App.Tests.UI; /// /// Fix round B item 15. Every existing MossTank markup pin /// (MossTankMarkupContractTests) validates mosstank*.xml /// against through reflection alone — "does a /// public property with this name and this CLR type exist" — never through /// itself, the code that actually mounts /// a plugin panel at runtime. MarkupDocument.Build has its own /// validation a reflection-only check can't see (attribute-format /// exceptions like ValidateArtStyle's "must be plain or retail", /// delegate-shape mismatches surfaced as thrown s /// rather than a missing property, numeric-attribute parsing, column-type /// dispatch). Before this test, a markup bug of that shape would throw /// inside RetailUiRuntime.MountPlugins's own try/catch and the panel /// would simply not appear — no test failure, no visible error short of a /// live client screenshot. This builds every real mosstank*.xml file /// against a REAL (a stub , /// same shape as MossTankMarkupContractTests.StubHost) so a bad /// attribute fails a test instead of dropping the panel silently at mount. /// public sealed class MossTankMarkupBuildOverRealFilesTests { private static string MossTankMarkupDirectory => Path.Combine(AppContext.BaseDirectory, "MossTank"); public static IEnumerable MossTankMarkupFiles() => Directory.GetFiles(MossTankMarkupDirectory, "mosstank*.xml") .OrderBy(static path => path, StringComparer.Ordinal) .Select(static path => new object[] { path }); [Theory] [MemberData(nameof(MossTankMarkupFiles))] public void EveryMossTankPanelFileBuildsAgainstARealPanelWithNoException(string path) { string xml = File.ReadAllText(path); var panel = new MossTankPanel(new StubHost()); UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32)); Assert.NotNull(built); Assert.NotEmpty(built.Children); } /// /// Round D item 4's own re-layout proof: the Monsters tab's real /// mosstank.xml list carries anchor="left right top bottom" /// (EveryStretchingListDeclaresARealAnchor, MossTankMarkupContractTests, /// pins the attribute is present; this proves the attribute actually /// DOES something through the real anchor machinery) — widening the /// built root panel widens the Monsters list in turn, the same /// mechanism MarkupResizableAnchorTests.ResizingPanel_LeftRightList_ /// WidensWithThePanel proves against synthetic markup, now proven /// against the real shipped file. /// [Fact] public void WideningTheRealMainPanelWidensTheRealMonstersList() { string xml = File.ReadAllText( Path.Combine(MossTankMarkupDirectory, "mosstank.xml")); var panel = new MossTankPanel(new StubHost()); UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32)); // Every tab's group is visibility-bound ("visible={XVisible}") to // StubHost's own IsAvailable=false automation, so relying on the // normal VisibleSource/TickSelfAndChildren reconciliation would // hide every tab (including the root). This test cares about the // anchor mechanism, not the tab-switching one — it makes the // Monsters group (the 4th of the nine tab groups in file order: // Options/Profiles/Vitals/Monsters/...) visible directly. Exact // type match, not OfType() — UiSimpleButton/ // UiMarkupTabButton (the tab strip) are ALSO UiPanel subtypes; // only a bare compiles to the base UiPanel type itself. UiPanel[] tabGroups = built.Children .Where(static child => child.GetType() == typeof(UiPanel)) .Cast() .ToArray(); Assert.Equal(9, tabGroups.Length); foreach (UiPanel group in tabGroups) group.Visible = false; UiPanel monstersGroup = tabGroups[3]; monstersGroup.Visible = true; UiMarkupList monstersList = Assert.Single(monstersGroup.Children.OfType()); var device = new RecordingGpuDevice(); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(1400f, 900f)); var ctx = new UiRenderContext(renderer, new Vector2(1400f, 900f)); // First draw at the authored (already-enlarged, 984 wide) default // captures the list's anchor baseline. built.DrawSelfAndChildren(ctx); float widthAtAuthoredDefault = monstersList.Width; // A live drag-resize (RetailWindowManager.ResizeTo) mutates Width // directly; the next draw re-applies the captured margins against // the NEW panel width. built.Width += 100f; built.DrawSelfAndChildren(ctx); Assert.True( monstersList.Width > widthAtAuthoredDefault, $"Monsters list width did not grow: {widthAtAuthoredDefault} -> {monstersList.Width}"); } /// /// D-3 (round E architecture re-check): mosstank.xml's own top-of-file /// comment already claimed the Advanced Options popup "gets its own /// resizable=\"true\" + anchored lists" — the popup file itself still /// carried the dead resize="none" attribute (only meaningful /// once resizable="true" already arms the master switch, /// MarkupDocument.cs), so the popup was permanently fixed-size. This /// proves the fix through the real anchor machinery (the same /// mechanism /// proves for the main panel): lOptionList grows in WIDTH ONLY /// (mosstank-advanced.xml's own comment explains why height growth is /// unsafe here — the value field sits directly below with no slack), /// and lFilterList tracks the growing right edge in lockstep so the /// widening option list never walks into it (the same "right-pinned /// sibling repositions" pattern the main panel already uses). /// [Fact] public void WideningTheRealAdvancedOptionsPopupGrowsTheOptionListWithoutOverlappingItsSibling() { string xml = File.ReadAllText( Path.Combine(MossTankMarkupDirectory, "mosstank-advanced.xml")); var panel = new MossTankPanel(new StubHost()); UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32)); Assert.True(built.Resizable); Assert.Equal(392f, built.MinWidth); // Round F item 1 shrunk the popup back to VTank's real // AdvancedOptionsView footprint (392x300) after deleting the // "Editing X." notice and the "MossTank Extras" section. Assert.Equal(300f, built.MinHeight); // Bypass the VisibleSource binding (bound to AdvancedOptionsVisible, // false on the stub automation) the same way the Monsters test // bypasses tab visibility — DrawSelfAndChildren's own anchor pass // never runs for an invisible element. built.Visible = true; UiMarkupList optionList = Assert.Single( built.Children.OfType(), static list => list.Width == 256f); UiMarkupList categoryList = Assert.Single( built.Children.OfType(), static list => list.Width == 120f); var device = new RecordingGpuDevice(); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(1400f, 900f)); var ctx = new UiRenderContext(renderer, new Vector2(1400f, 900f)); built.DrawSelfAndChildren(ctx); float optionListWidthBefore = optionList.Width; float optionListHeightBefore = optionList.Height; float categoryListLeftBefore = categoryList.Left; float categoryListWidthBefore = categoryList.Width; built.Width += 100f; built.DrawSelfAndChildren(ctx); Assert.True( optionList.Width > optionListWidthBefore, $"Option list width did not grow: {optionListWidthBefore} -> {optionList.Width}"); Assert.Equal(optionListHeightBefore, optionList.Height); // height fixed Assert.True( categoryList.Left > categoryListLeftBefore, $"Category list did not track the growing right edge: {categoryListLeftBefore} -> {categoryList.Left}"); Assert.Equal(categoryListWidthBefore, categoryList.Width); // width fixed, only repositions Assert.True( optionList.Left + optionList.Width <= categoryList.Left, $"Widened option list (right edge {optionList.Left + optionList.Width}) overlaps " + $"the repositioned category list (left edge {categoryList.Left})."); } /// /// D-6 (round E architecture re-check): every real-file re-layout pin /// so far only inspects ONE named element after a resize (the /// Monsters list, the Advanced Options option/category lists). This /// proves the whole resolved tree, at both ends of the main panel's /// declared resize range — the floor (minw/minh, 856x236) and one /// enlarged size (1100x320) past the 984x271 authored default — has /// no sibling overlap and no child crossing its parent's edge, for /// EACH of the nine tabs in turn (only the active tab's own subtree /// gets a real anchor pass; DrawSelfAndChildren returns early for an /// invisible element, matching 's /// own reasoning). Reuses the same rectangle-overlap/parent-bounds /// algorithm MossTankMarkupContractTests' AssertNoSiblingOverlap/ /// AssertWithinParent apply to the AUTHORED XElement tree, adapted /// here to the RESOLVED UiElement tree (real Left/Top/Width/Height /// post-anchor, not the raw x/y/w/h attributes) since a resize only /// exists at this level. /// [Theory] [InlineData(856f, 236f)] // the panel's own minw/minh floor [InlineData(1100f, 320f)] // one enlarged size past the 984x271 default public void ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize( float width, float height) { string xml = File.ReadAllText( Path.Combine(MossTankMarkupDirectory, "mosstank.xml")); var device = new RecordingGpuDevice(); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(1400f, 900f)); var ctx = new UiRenderContext(renderer, new Vector2(1400f, 900f)); // A fresh MarkupDocument.Build per tab, not one shared tree reused // across all nine: each element's anchor margins are captured ONCE, // on its very first ApplyAnchor call, and never recomputed after — // reusing one tree across tabs would let an earlier tab's resize // leave the shared root already at the target size by the time a // LATER tab's own descendants take their "first" (authored-default) // capture, corrupting their baseline. for (int tabIndex = 0; tabIndex < 9; tabIndex++) { var panel = new MossTankPanel(new StubHost()); UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32)); built.Visible = true; UiPanel[] tabGroups = built.Children .Where(static child => child.GetType() == typeof(UiPanel)) .Cast() .ToArray(); Assert.Equal(9, tabGroups.Length); foreach (UiPanel group in tabGroups) group.Visible = false; tabGroups[tabIndex].Visible = true; // First draw at the authored default captures every anchored // descendant's baseline margins (matching a live window that // just opened); only THEN does resizing to the target size // reapply those captured margins against the new bounds — // the same two-draw shape WideningTheRealMainPanelWidensTheRealMonstersList // uses. built.DrawSelfAndChildren(ctx); built.Width = width; built.Height = height; built.DrawSelfAndChildren(ctx); AssertResolvedWithinParent(built); AssertResolvedNoSiblingOverlap(built); } } private static void AssertResolvedWithinParent(UiElement parent) { foreach (UiElement child in parent.Children) { // An invisible child's OWN box (e.g. an inactive tab group, // whose top-level anchor still tracks the resize regardless // of visibility) is still worth checking, but DrawSelfAndChildren // returns before applying anchor to ITS descendants while it // stays invisible — recursing further would compare stale, // never-resolved-for-this-size geometry and misreport it as // broken. Skip descending past an invisible node; the tab it // belongs to gets its own real pass in this test's own loop // once it becomes the active one. if (!child.Visible) continue; // UiLabel is excluded from the size checks (not from // recursion): UiLabel.OnDraw (UiPanel.cs, the CT-GF1 fix) // deliberately overwrites Width/Height to the CURRENT measured // text extent on every draw, ignoring its own authored w/h — // "a markup