test(vtank): slice 7 round E item D-6 — resolved-geometry pin at minw/enlarged size

Every real-file re-layout pin so far inspects ONE named element after a
resize (the Monsters list, the Advanced Options option/category
lists). Adds a whole-tree pin at both ends of the main panel's declared
resize range — the minw/minh floor (856x236) and one enlarged size
(1100x320) past the 984x271 default — for each of the nine tabs in
turn: builds the real mosstank.xml through MarkupDocument.Build, draws
once at the authored default to capture anchor baselines, resizes, and
asserts no sibling overlap and no child crossing its parent's edge.
Adapts MossTankMarkupContractTests' AssertNoSiblingOverlap/
AssertWithinParent algorithm (rectangle intersection, ignore
touching/zero-size boxes, two bare <group> tab pages never overlap-
check each other) from the authored XElement tree to the resolved
UiElement tree (real Left/Top/Width/Height post-anchor), since a resize
only exists at that level.

Two real bugs surfaced and fixed in the test itself while building this:
(1) reusing one built tree across all nine tabs let an earlier tab's
resize leave the shared root already at the target size by the time a
later tab's own descendants took their "first" (baseline) anchor
capture — fixed by building a fresh tree per tab; (2) walking into an
INVISIBLE tab group's descendants compared their stale, never-
anchored-for-this-size geometry against the group's own (correctly
resized) bounds — fixed by skipping recursion past an invisible node,
since it never got a real anchor pass this round. A genuine false
positive from the test harness itself was also found and excluded:
UiLabel.OnDraw (the CT-GF1 fix) always overwrites Width/Height to the
current measured text extent, and this stub renderer has no real
DatFont, so it falls through to a crude "text.Length * 7f" placeholder
far wider than the authored box or the real DAT font would ever
measure — labels are excluded from the size checks (position still
checked via recursion) for the same reason D-3's own comment cites this
constraint.

Mutation shown to fail first: removing the Monsters list's
anchor="left right top bottom" attribute (temporary edit, reverted —
net diff on mosstank.xml is zero) reproduced "UiMarkupList @
(0,16,976,151) crosses the right edge of UiPanel (w=848)" at the 856
floor, exactly the failure this pin exists to catch.

App markup/plugin filter 244 -> 246; MossTank suite holds 722/722
(App-side test only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 18:25:16 +02:00
parent 1a0b0e014c
commit 7badbee889

View file

@ -181,6 +181,168 @@ public sealed class MossTankMarkupBuildOverRealFilesTests
+ $"the repositioned category list (left edge {categoryList.Left}).");
}
/// <summary>
/// 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 <see cref="WideningTheRealMainPanelWidensTheRealMonstersList"/>'s
/// own reasoning). Reuses the same rectangle-overlap/parent-bounds
/// algorithm <c>MossTankMarkupContractTests</c>' 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.
/// </summary>
[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<UiPanel>()
.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 <label> authors position only". This test's stub
// renderer has no real DatFont, so a label falls through to
// OnDraw's crude "text.Length * 7f" placeholder, which reports
// a box far wider than the authored geometry (or the real DAT
// font) ever would and would misreport as crossing a parent
// edge or overlapping a sibling — noise from the test harness,
// not a real layout bug. Left/Top (position) are unaffected by
// OnDraw and still worth checking through the parent-bounds
// recursion.
if (child is not UiLabel)
{
if (child.Width > 0f)
{
Assert.True(
child.Left + child.Width <= parent.Width + 0.01f,
$"{child.GetType().Name} @ ({child.Left},{child.Top},{child.Width},"
+ $"{child.Height}) crosses the right edge of {parent.GetType().Name} "
+ $"(w={parent.Width}).");
}
if (child.Height > 0f)
{
Assert.True(
child.Top + child.Height <= parent.Height + 0.01f,
$"{child.GetType().Name} @ ({child.Left},{child.Top},{child.Width},"
+ $"{child.Height}) crosses the bottom edge of {parent.GetType().Name} "
+ $"(h={parent.Height}).");
}
}
AssertResolvedWithinParent(child);
}
}
private static void AssertResolvedNoSiblingOverlap(UiElement container)
{
// Same "invisible subtree never got a real anchor pass" reasoning
// as AssertResolvedWithinParent — only compare/recurse into
// children that are actually visible.
UiElement[] children = container.Children
.Where(static child => child.Visible)
.ToArray();
for (int i = 0; i < children.Length; i++)
{
for (int j = i + 1; j < children.Length; j++)
{
UiElement a = children[i], b = children[j];
// Exact type match, not "is UiPanel": bare <group> tab
// pages deliberately occupy the identical rect (only one
// is Visible at a time) — subtypes like UiMarkupTabButton
// are real siblings that must not overlap. UiLabel is
// excluded for the same font-dependent-box reason
// AssertResolvedWithinParent excludes it (see its comment).
if (a.GetType() == typeof(UiPanel) && b.GetType() == typeof(UiPanel))
continue;
if (a is UiLabel || b is UiLabel)
continue;
Assert.True(
!ResolvedRectanglesOverlap(a, b),
$"{a.GetType().Name} @ ({a.Left},{a.Top},{a.Width},{a.Height}) overlaps "
+ $"sibling {b.GetType().Name} @ ({b.Left},{b.Top},{b.Width},{b.Height}).");
}
}
foreach (UiElement child in children)
AssertResolvedNoSiblingOverlap(child);
}
private static bool ResolvedRectanglesOverlap(UiElement a, UiElement b)
{
if (a.Width <= 0f || a.Height <= 0f || b.Width <= 0f || b.Height <= 0f)
return false; // an element with no resolved size never "occupies" space
return a.Left < b.Left + b.Width && b.Left < a.Left + a.Width
&& a.Top < b.Top + b.Height && b.Top < a.Top + a.Height;
}
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
{
public IGpuFrame? CurrentFrame => null;