fix(vt): list column fix round 9/11 — MossTankMarkupContractTests learns <column>
The contract test suite's per-element binding-shape check treated
EVERY element's onclick as a plain Action, and its interactive-element
set (used both for "declares a real handler" and "has an accessible
tooltip") had no entry for <column> at all — both would misvalidate a
future mosstank.xml <column> the way Campaign VT slice 1 Part B and
this fix round's item 1 actually shipped it (onchange/onclick as
Action<int>, the row index).
Extracted the per-element dispatch out of
EveryInteractiveBindingMatchesTheRetainedUiDelegateShape into
AssertElementBindingsMatchRetainedUiDelegateShape, which now
special-cases "column": both onchange and onclick must resolve to
Action<int>, never plain Action. The interactive-element name list is
now the single shared InteractiveElementNames array (was duplicated
inline in EveryInteractiveControlDeclaresARealHandlerBinding and
TextlessAndAbbreviatedControlsHaveAccessibleRetailTooltips), with
"column" added — zero behavior change against mosstank.xml today (it
has no <column> elements yet), confirmed by the existing 190-control
count assertion staying green.
New tests: InteractiveElementNames_IncludesColumn pins the addition
directly; Column_OnchangeAndOnclick_MustBeActionOfInt builds a
synthetic <column> (not loaded from mosstank.xml, which has none) and
proves both a correctly-typed Action<int> onchange passes and a
column onclick bound to a plain Action (the shape every OTHER
element's onclick uses) is rejected — confirmed by temporarily
removing the "column" special case and rerunning: the plain-Action
onclick then passed silently ("No exception was thrown", the
generic Action check being satisfied by a real Action property),
proving the test only passes because the fix's column-specific
Action<int> enforcement is present.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
8fb4909cc8
commit
0f79ee03ee
1 changed files with 97 additions and 24 deletions
|
|
@ -6,6 +6,29 @@ namespace AcDream.Plugins.MossTank.Tests;
|
||||||
|
|
||||||
public sealed class MossTankMarkupContractTests
|
public sealed class MossTankMarkupContractTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fix round item 9: every element name whose interactive attributes
|
||||||
|
/// (onclick/onchange/onsubmit) this contract validates and requires a
|
||||||
|
/// real handler for — shared by
|
||||||
|
/// <see cref="EveryInteractiveControlDeclaresARealHandlerBinding"/> and
|
||||||
|
/// <see cref="TextlessAndAbbreviatedControlsHaveAccessibleRetailTooltips"/>.
|
||||||
|
/// <c>column</c> (Campaign VT slice 1 Part B's <c><list><column></c>)
|
||||||
|
/// joined this set here — mosstank.xml itself has no <c><column></c>
|
||||||
|
/// elements yet, so this is a zero-behavior-change addition against the
|
||||||
|
/// current file (see <see cref="InteractiveElementNames_IncludesColumn"/>
|
||||||
|
/// for the direct pin).
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string[] InteractiveElementNames =
|
||||||
|
[
|
||||||
|
"tab", "button", "toggle", "slider", "field", "menu", "list", "column",
|
||||||
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InteractiveElementNames_IncludesColumn()
|
||||||
|
{
|
||||||
|
Assert.Contains("column", InteractiveElementNames);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VtankTabOrderAndEveryBindingResolveAgainstTheLivePanel()
|
public void VtankTabOrderAndEveryBindingResolveAgainstTheLivePanel()
|
||||||
{
|
{
|
||||||
|
|
@ -54,24 +77,80 @@ public sealed class MossTankMarkupContractTests
|
||||||
StringComparer.Ordinal);
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
foreach (XElement element in root.DescendantsAndSelf())
|
foreach (XElement element in root.DescendantsAndSelf())
|
||||||
{
|
AssertElementBindingsMatchRetainedUiDelegateShape(element, byName);
|
||||||
AssertBindingType(element, "onclick", typeof(Action), byName);
|
}
|
||||||
AssertBindingType(
|
|
||||||
element,
|
|
||||||
"onsubmit",
|
|
||||||
typeof(Action<string>),
|
|
||||||
byName);
|
|
||||||
|
|
||||||
Type? changeType = element.Name.LocalName switch
|
/// <summary>
|
||||||
{
|
/// Fix round item 9: <c><column></c>'s own <c>onchange</c> (a
|
||||||
"field" or "menu" => typeof(Action<string>),
|
/// <c>type="check"</c> column) and <c>onclick</c> (<c>type="icon"</c>,
|
||||||
"slider" => typeof(Action<float>),
|
/// or a <c>type="text"</c> column's fix-item-1 optional onclick) are
|
||||||
"list" => typeof(Action<int>),
|
/// BOTH <c>Action<int></c> (the row index) — never the plain
|
||||||
_ => null,
|
/// <c>Action</c> every other element's <c>onclick</c> resolves to.
|
||||||
};
|
/// Extracted out of <see cref="EveryInteractiveBindingMatchesTheRetainedUiDelegateShape"/>
|
||||||
if (changeType is not null)
|
/// so <see cref="Column_OnchangeAndOnclick_MustBeActionOfInt"/> can drive
|
||||||
AssertBindingType(element, "onchange", changeType, byName);
|
/// it directly against a synthetic <c><column></c> element —
|
||||||
|
/// mosstank.xml itself has none yet.
|
||||||
|
/// </summary>
|
||||||
|
private static void AssertElementBindingsMatchRetainedUiDelegateShape(
|
||||||
|
XElement element,
|
||||||
|
IReadOnlyDictionary<string, PropertyInfo> byName)
|
||||||
|
{
|
||||||
|
if (element.Name.LocalName == "column")
|
||||||
|
{
|
||||||
|
AssertBindingType(element, "onchange", typeof(Action<int>), byName);
|
||||||
|
AssertBindingType(element, "onclick", typeof(Action<int>), byName);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AssertBindingType(element, "onclick", typeof(Action), byName);
|
||||||
|
AssertBindingType(
|
||||||
|
element,
|
||||||
|
"onsubmit",
|
||||||
|
typeof(Action<string>),
|
||||||
|
byName);
|
||||||
|
|
||||||
|
Type? changeType = element.Name.LocalName switch
|
||||||
|
{
|
||||||
|
"field" or "menu" => typeof(Action<string>),
|
||||||
|
"slider" => typeof(Action<float>),
|
||||||
|
"list" => typeof(Action<int>),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
if (changeType is not null)
|
||||||
|
AssertBindingType(element, "onchange", changeType, byName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ColumnBindingProbe
|
||||||
|
{
|
||||||
|
public Action<int> RowAction { get; } = _ => { };
|
||||||
|
public Action PlainAction { get; } = () => { };
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Column_OnchangeAndOnclick_MustBeActionOfInt()
|
||||||
|
{
|
||||||
|
var byName = typeof(ColumnBindingProbe)
|
||||||
|
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.ToDictionary(static property => property.Name, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
// Correctly typed Action<int> — must not throw.
|
||||||
|
var goodColumn = new XElement(
|
||||||
|
"column",
|
||||||
|
new XAttribute("type", "check"),
|
||||||
|
new XAttribute("onchange", "{RowAction}"));
|
||||||
|
AssertElementBindingsMatchRetainedUiDelegateShape(goodColumn, byName);
|
||||||
|
|
||||||
|
// A column's onclick bound to a PLAIN Action (the shape every other
|
||||||
|
// element's onclick uses) must be rejected — proves the dispatch
|
||||||
|
// actually enforces Action<int> for <column> specifically, rather
|
||||||
|
// than silently accepting whatever the generic non-column path
|
||||||
|
// would have allowed.
|
||||||
|
var badColumn = new XElement(
|
||||||
|
"column",
|
||||||
|
new XAttribute("type", "icon"),
|
||||||
|
new XAttribute("onclick", "{PlainAction}"));
|
||||||
|
Assert.Throws<Xunit.Sdk.EqualException>(
|
||||||
|
() => AssertElementBindingsMatchRetainedUiDelegateShape(badColumn, byName));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -80,10 +159,7 @@ public sealed class MossTankMarkupContractTests
|
||||||
XDocument document = XDocument.Load(
|
XDocument document = XDocument.Load(
|
||||||
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
|
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
|
||||||
XElement root = Assert.IsType<XElement>(document.Root);
|
XElement root = Assert.IsType<XElement>(document.Root);
|
||||||
HashSet<string> interactive = new(
|
HashSet<string> interactive = new(InteractiveElementNames, StringComparer.Ordinal);
|
||||||
[
|
|
||||||
"tab", "button", "toggle", "slider", "field", "menu", "list",
|
|
||||||
], StringComparer.Ordinal);
|
|
||||||
|
|
||||||
XElement[] controls = root.Descendants()
|
XElement[] controls = root.Descendants()
|
||||||
.Where(element => interactive.Contains(element.Name.LocalName))
|
.Where(element => interactive.Contains(element.Name.LocalName))
|
||||||
|
|
@ -134,10 +210,7 @@ public sealed class MossTankMarkupContractTests
|
||||||
XDocument document = XDocument.Load(
|
XDocument document = XDocument.Load(
|
||||||
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
|
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
|
||||||
XElement root = Assert.IsType<XElement>(document.Root);
|
XElement root = Assert.IsType<XElement>(document.Root);
|
||||||
string[] interactive =
|
string[] interactive = InteractiveElementNames;
|
||||||
[
|
|
||||||
"tab", "button", "toggle", "slider", "field", "menu", "list",
|
|
||||||
];
|
|
||||||
HashSet<string> terse = new(
|
HashSet<string> terse = new(
|
||||||
[
|
[
|
||||||
"+", "-", "↑", "↓", "F", "B", "G", "I", "Y", "V", "A",
|
"+", "-", "↑", "↓", "F", "B", "G", "I", "Y", "V", "A",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue