fix(vtank): slice 7 fix round C item D3 — bottom-band pin counts implicit widget height

AssertWithinParent only flagged a child crossing a group's bottom edge
when that child declared its own h attribute — Number(child, "h") reads
0 for an absent attribute, so an unsized <label>/<field>/<toggle>/
<button> positioned right against a group's bottom edge silently passed
even though the retained-UI runtime still gives it a real default row
height at draw time. Added EffectiveHeight: falls back to each widget
kind's own implicit default when h is absent (label/field 16, toggle 20,
button 16 as a defensive floor since real buttons in this markup range
16-25px and always declare h explicitly); <list>/<menu> keep the old
"0 when absent" behavior since they have no implicit default at all.

Mutation named: temporarily reverted the check back to Number(child, "h")
and confirmed the new AssertWithinParent_CatchesAnUnsizedLabelNearTheBottomEdge
pin fails (no exception thrown for a synthetic <label y="190"> with no h
inside a 194-tall group, which should clip 12px past the bottom once the
16px default is counted) before restoring the fix. Audited every real
mosstank*.xml file for label/toggle/field/button elements missing h
(grep for each without ` h="`) — none exist, so this stricter check
introduces no new failures against the shipped markup.

MossTank suite 679 -> 680 (one new pin). Full solution build green; App
markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 15:30:43 +02:00
parent f15667db5f
commit 3b8d021946

View file

@ -331,6 +331,26 @@ public sealed class MossTankMarkupContractTests
AssertWithinParent(root);
}
/// <summary>
/// Fix round C item D3: direct pin on <see cref="AssertWithinParent"/>'s
/// height check, independent of any real markup file. A
/// <c>&lt;label y="190"&gt;</c> with no declared <c>h</c> inside a
/// 194-tall group clips 12px past the bottom edge once the label's
/// real 16px default row height is counted (190 + 16 = 206 > 194) —
/// before this fix, an absent <c>h</c> read as zero height and the
/// check silently passed.
/// </summary>
[Fact]
public void AssertWithinParent_CatchesAnUnsizedLabelNearTheBottomEdge()
{
var group = new XElement("group",
new XAttribute("w", "848"), new XAttribute("h", "194"),
new XElement("label", new XAttribute("x", "4"), new XAttribute("y", "190"),
new XAttribute("text", "Notice")));
Assert.Throws<Xunit.Sdk.TrueException>(() => AssertWithinParent(group));
}
/// <summary>
/// Fix round B item 2: <see cref="AssertWithinParent"/> only ever proved
/// a control fits inside its OWN parent's declared bounds — it never
@ -571,7 +591,7 @@ public sealed class MossTankMarkupContractTests
foreach (XElement child in parent.Elements())
{
float width = Number(child, "w");
float height = Number(child, "h");
float height = EffectiveHeight(child);
if (width > 0f)
{
Assert.True(
@ -588,6 +608,38 @@ public sealed class MossTankMarkupContractTests
}
}
/// <summary>
/// Fix round C item D3: an element's declared <c>h</c> only tells the
/// whole story for widget kinds that never rely on an implicit
/// default. <c>&lt;label&gt;</c>/<c>&lt;field&gt;</c> without <c>h</c>
/// still occupy one DAT-font text row (16px) at runtime, and
/// <c>&lt;toggle&gt;</c> still occupies the checkbox lamp's own 20px
/// footprint — <see cref="AssertWithinParent"/> used to treat any of
/// those as zero-height (h absent) and silently pass an unsized notice
/// label positioned right up against a group's bottom edge, which
/// actually clips in the running client. <c>&lt;button&gt;</c> has no
/// single canonical default (real buttons in this markup range from
/// 16 to 25px and always declare h explicitly), so 16 — the same
/// floor as label/field — is used defensively; <c>&lt;list&gt;</c>/
/// <c>&lt;menu&gt;</c> have no implicit default at all and keep the
/// old "0 when absent" behavior.
/// </summary>
private static float EffectiveHeight(XElement element)
{
float declared = Number(element, "h");
if (declared > 0f)
return declared;
return element.Name.LocalName switch
{
"label" => 16f,
"field" => 16f,
"toggle" => 20f,
"button" => 16f,
_ => 0f,
};
}
private static float Number(XElement element, string attribute) =>
float.TryParse(
(string?)element.Attribute(attribute),