fix(vt): list column fix round 10/11 — Meta/Monsters-shaped end-to-end tests
Two tests built through the full MarkupDocument.Build parse -> bind -> draw (recording renderer) -> click pipeline, transcribing the two real VTank list shapes named in the fix round brief (refs/vtank/uTank2.ViewXML.mainView.xml): - EndToEnd_MetaShapedSixColumnList: lstMetaRules's shape (3 icon columns, a 150px text column, then two width="*" text columns, the second with an onclick) at a list width (703) chosen so the two auto columns' 505px leftover does NOT divide evenly (252/253) — proving the last one absorbs the rounding slack. Asserts each column kind draws inside its own x-range and a click in each of the six columns reaches its own bound callback with the right row (icon columns fire their own onclick; the plain text column still selects; the onclick-bearing trailing text column fires that instead). - EndToEnd_MonstersShapedTwentyThreeColumnList: lstMonsters's shape (14 check columns, 7 text columns each given an onclick per fix item 1, 2 icon columns) built from real fixedwidth values with NO "*" anywhere, at a list width (724) equal to the exact sum of every column's declared width — the "no auto column, last absorbs the remainder as today" case. Asserts the column-kind counts, each drawn kind's x-range, and one representative click per kind reaches its own callback (other same-kind columns share a dummy callback, proving the bound-per-column dispatch, not just "some column of that kind fired"). Both use iconkind="item" for their icon columns (not the default "did") so the fake resolver's echoed id is the literal texture drawn, sidestepping PluginIcons.Normalize's did-namespace OR — an incidental discovery while writing these (the first draft asserted on raw ids and failed against the normalized 0x06000000-tagged textures). Verified meaningful two ways: both failed first against the pre-fix raw-id assumption (texture ids came back normalized), and — a stronger check specific to these integration tests, since the underlying mechanics were already covered by earlier fix-round commits — a temporary mutation of ColumnLayout's auto-column share computation (forcing share=0) made EndToEnd_MetaShapedSixColumnList fail exactly as expected before being reverted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
0f79ee03ee
commit
e178c6abac
1 changed files with 217 additions and 0 deletions
|
|
@ -1182,4 +1182,221 @@ public sealed class MarkupListColumnsTests
|
|||
|
||||
Assert.Null(viaMarkup.Columns);
|
||||
}
|
||||
|
||||
// ── Fix round: end-to-end MarkupDocument builds (fix item 10) ───────────
|
||||
|
||||
private static UiDatFont MakeAsciiFont()
|
||||
{
|
||||
var glyphs = new Dictionary<char, FontCharDesc>();
|
||||
foreach (char c in "ABCDEFGH")
|
||||
glyphs[c] = new FontCharDesc { Unicode = c, Width = 6, Height = 8 };
|
||||
return new UiDatFont(
|
||||
fgTex: 1u, fgW: 32, fgH: 32,
|
||||
bgTex: 0, bgW: 0, bgH: 0,
|
||||
lineHeight: 16f, baselineOffset: 12f,
|
||||
glyphs);
|
||||
}
|
||||
|
||||
private sealed class MetaShapedBinding
|
||||
{
|
||||
public IReadOnlyList<uint> Delete { get; } = new uint[] { 101u, 102u };
|
||||
public IReadOnlyList<uint> MoveUp { get; } = new uint[] { 201u, 202u };
|
||||
public IReadOnlyList<uint> MoveDown { get; } = new uint[] { 301u, 302u };
|
||||
public IReadOnlyList<string> State { get; } = new[] { "A", "B" };
|
||||
public IReadOnlyList<string> Condition { get; } = new[] { "C", "D" };
|
||||
public IReadOnlyList<string> Action { get; } = new[] { "E", "F" };
|
||||
public int Selected { get; set; } = -1;
|
||||
public List<int> Selections { get; } = new();
|
||||
public Action<int> SelectRow => row => Selections.Add(row);
|
||||
public List<int> DeleteClicks { get; } = new();
|
||||
public Action<int> ClickDelete => row => DeleteClicks.Add(row);
|
||||
public List<int> MoveUpClicks { get; } = new();
|
||||
public Action<int> ClickMoveUp => row => MoveUpClicks.Add(row);
|
||||
public List<int> MoveDownClicks { get; } = new();
|
||||
public Action<int> ClickMoveDown => row => MoveDownClicks.Add(row);
|
||||
public List<int> ActionClicks { get; } = new();
|
||||
public Action<int> ClickAction => row => ActionClicks.Add(row);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VTank's Meta tab (<c>refs/vtank/uTank2.ViewXML.mainView.xml</c>'s
|
||||
/// <c>lstMetaRules</c>, ~line 292): 3 icon columns (16px each), a 150px
|
||||
/// text column, then TWO trailing 0-width (auto) text columns — the real
|
||||
/// VVS shape our <c>width="*"</c> convention exists to express. The list
|
||||
/// width (703) is chosen so the two auto columns' shares DON'T divide
|
||||
/// evenly (505/2 = 252 remainder 1), proving the last one absorbs the
|
||||
/// rounding slack rather than both getting an identical share.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EndToEnd_MetaShapedSixColumnList_AutoColumnsShareRemainder_EachKindRoutesCorrectly()
|
||||
{
|
||||
var resolver = new FakeIconResolver();
|
||||
var binding = new MetaShapedBinding();
|
||||
const string xml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"720\" h=\"140\">" +
|
||||
"<list x=\"0\" y=\"0\" w=\"703\" h=\"60\" rowheight=\"18\" " +
|
||||
"selected=\"{Selected}\" onchange=\"{SelectRow}\">" +
|
||||
" <column type=\"icon\" width=\"16\" iconkind=\"item\" values=\"{Delete}\" onclick=\"{ClickDelete}\"/>" +
|
||||
" <column type=\"icon\" width=\"16\" iconkind=\"item\" values=\"{MoveUp}\" onclick=\"{ClickMoveUp}\"/>" +
|
||||
" <column type=\"icon\" width=\"16\" iconkind=\"item\" values=\"{MoveDown}\" onclick=\"{ClickMoveDown}\"/>" +
|
||||
" <column type=\"text\" width=\"150\" items=\"{State}\"/>" +
|
||||
" <column type=\"text\" width=\"*\" items=\"{Condition}\"/>" +
|
||||
" <column type=\"text\" width=\"*\" items=\"{Action}\" onclick=\"{ClickAction}\"/>" +
|
||||
"</list></panel>";
|
||||
|
||||
var panel = MarkupDocument.Build(xml, binding, Sprite, datFont: MakeAsciiFont(), icons: resolver);
|
||||
var list = Assert.IsType<UiMarkupList>(panel.Children[0]);
|
||||
Assert.Equal(6, list.Columns!.Count);
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
UiMarkupListColumnKind.Icon, UiMarkupListColumnKind.Icon, UiMarkupListColumnKind.Icon,
|
||||
UiMarkupListColumnKind.Text, UiMarkupListColumnKind.Text, UiMarkupListColumnKind.Text,
|
||||
},
|
||||
list.Columns.Select(c => c.Kind));
|
||||
|
||||
var (renderer, ctx) = MakeContext(800f, 200f);
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
// Column ranges: [0,16) [16,32) [32,48) [48,198) [198,450) [450,703).
|
||||
// The two auto text columns split 505 leftover px as 252/253 — the
|
||||
// LAST one (Action) absorbs the extra pixel.
|
||||
var deleteQuad = Assert.Single(renderer.DebugSpriteSegmentVerts, s => s.Texture == 101u);
|
||||
Assert.True(deleteQuad.Verts[0] < 16f);
|
||||
var moveUpQuad = Assert.Single(renderer.DebugSpriteSegmentVerts, s => s.Texture == 201u);
|
||||
Assert.True(moveUpQuad.Verts[0] >= 16f && moveUpQuad.Verts[0] < 32f);
|
||||
var moveDownQuad = Assert.Single(renderer.DebugSpriteSegmentVerts, s => s.Texture == 301u);
|
||||
Assert.True(moveDownQuad.Verts[0] >= 32f && moveDownQuad.Verts[0] < 48f);
|
||||
|
||||
var glyphs = renderer.DebugSpriteSegmentVerts
|
||||
.Where(s => s.Texture == 1u)
|
||||
.SelectMany(s => Chunk(s.Verts))
|
||||
.ToList();
|
||||
Assert.Contains(glyphs, v => v[0] >= 48f && v[0] < 198f); // State
|
||||
Assert.Contains(glyphs, v => v[0] >= 198f && v[0] < 450f); // Condition (auto, 252px)
|
||||
Assert.Contains(glyphs, v => v[0] >= 450f && v[0] < 703f); // Action (auto, 253px — the slack)
|
||||
|
||||
// Click routing: each column kind reaches its own callback with the
|
||||
// right row; the plain text column (State) still selects.
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 8, Data2 = 0 }); // col0 row0
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 24, Data2 = 18 }); // col1 row1
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 40, Data2 = 0 }); // col2 row0
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 60, Data2 = 18 }); // col3 (State) row1
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 600, Data2 = 0 }); // col5 (Action) row0
|
||||
|
||||
Assert.Equal(new[] { 0 }, binding.DeleteClicks);
|
||||
Assert.Equal(new[] { 1 }, binding.MoveUpClicks);
|
||||
Assert.Equal(new[] { 0 }, binding.MoveDownClicks);
|
||||
Assert.Equal(new[] { 1 }, binding.Selections); // col3 click selected row1
|
||||
Assert.Equal(new[] { 0 }, binding.ActionClicks);
|
||||
}
|
||||
|
||||
private sealed class MonsterShapedBinding
|
||||
{
|
||||
public IReadOnlyList<bool> Checks { get; } = new[] { true, false };
|
||||
public IReadOnlyList<string> Texts { get; } = new[] { "A", "B" };
|
||||
public IReadOnlyList<uint> Icons0 { get; } = new uint[] { 501u, 502u };
|
||||
public IReadOnlyList<uint> Icons1 { get; } = new uint[] { 601u, 602u };
|
||||
public int Selected { get; set; } = -1;
|
||||
|
||||
public List<(int Column, int Row)> Check0Fires { get; } = new();
|
||||
public Action<int> OnCheck0 => row => Check0Fires.Add((0, row));
|
||||
public Action<int> OnCheckRest => _ => { };
|
||||
|
||||
public List<(int Column, int Row)> Text0Fires { get; } = new();
|
||||
public Action<int> OnText0 => row => Text0Fires.Add((0, row));
|
||||
public Action<int> OnTextRest => _ => { };
|
||||
|
||||
public List<(int Column, int Row)> Icon0Fires { get; } = new();
|
||||
public Action<int> OnIcon0 => row => Icon0Fires.Add((0, row));
|
||||
public Action<int> OnIcon1 => _ => { };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VTank's Monsters tab (<c>refs/vtank/uTank2.ViewXML.mainView.xml</c>'s
|
||||
/// <c>lstMonsters</c>, ~lines 111-140): 14 check columns (16px each,
|
||||
/// clFester..clCorrosion), 7 text columns (clMonName 120, clPriority 20,
|
||||
/// clDamageType 56, clExVulnType 56, clWeaponToUse 80, clOffHand 80,
|
||||
/// clPetDamageType 56), then 2 icon columns (clMoveUp/clMoveDown, 16px
|
||||
/// each). Every declared width is a plain positive number — no
|
||||
/// <c>"*"</c> anywhere, matching the real file — so the LAST column
|
||||
/// (clMoveDown) still absorbs the remainder "as today", which this test
|
||||
/// pins by sizing the list to the EXACT sum of every column's declared
|
||||
/// width (724px): the last column ends up with exactly its own declared
|
||||
/// 16px, not stretched or starved.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EndToEnd_MonstersShapedTwentyThreeColumnList_EachKindRoutesCorrectly()
|
||||
{
|
||||
var resolver = new FakeIconResolver();
|
||||
var binding = new MonsterShapedBinding();
|
||||
|
||||
var columns = new System.Text.StringBuilder();
|
||||
for (int i = 0; i < 14; i++)
|
||||
{
|
||||
string onchange = i == 0 ? "{OnCheck0}" : "{OnCheckRest}";
|
||||
columns.Append(
|
||||
$"<column type=\"check\" width=\"16\" values=\"{{Checks}}\" onchange=\"{onchange}\"/>");
|
||||
}
|
||||
int[] textWidths = { 120, 20, 56, 56, 80, 80, 56 };
|
||||
for (int i = 0; i < textWidths.Length; i++)
|
||||
{
|
||||
string onclick = i == 0 ? "{OnText0}" : "{OnTextRest}";
|
||||
columns.Append(
|
||||
$"<column type=\"text\" width=\"{textWidths[i]}\" items=\"{{Texts}}\" onclick=\"{onclick}\"/>");
|
||||
}
|
||||
columns.Append("<column type=\"icon\" width=\"16\" iconkind=\"item\" values=\"{Icons0}\" onclick=\"{OnIcon0}\"/>");
|
||||
columns.Append("<column type=\"icon\" width=\"16\" iconkind=\"item\" values=\"{Icons1}\" onclick=\"{OnIcon1}\"/>");
|
||||
|
||||
string xml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"740\" h=\"140\">" +
|
||||
"<list x=\"0\" y=\"0\" w=\"724\" h=\"60\" rowheight=\"18\" selected=\"{Selected}\">" +
|
||||
columns +
|
||||
"</list></panel>";
|
||||
|
||||
var panel = MarkupDocument.Build(xml, binding, Sprite, datFont: MakeAsciiFont(), icons: resolver);
|
||||
var list = Assert.IsType<UiMarkupList>(panel.Children[0]);
|
||||
Assert.Equal(23, list.Columns!.Count);
|
||||
Assert.Equal(14, list.Columns.Count(c => c.Kind == UiMarkupListColumnKind.Check));
|
||||
Assert.Equal(7, list.Columns.Count(c => c.Kind == UiMarkupListColumnKind.Text));
|
||||
Assert.Equal(2, list.Columns.Count(c => c.Kind == UiMarkupListColumnKind.Icon));
|
||||
|
||||
var (renderer, ctx) = MakeContext(800f, 200f);
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
// col0 (check): [0,16). col14 (first text, clMonName): [224,344).
|
||||
// col21 (first icon, clMoveUp): [692,708). col22 (LAST, clMoveDown):
|
||||
// [708,724) — exactly its own declared 16px since every width here
|
||||
// is a plain fixedwidth number (no "*"), the "no auto column"
|
||||
// no-op-through-ColumnLayout case.
|
||||
var checkGlyph = renderer.DebugSpriteSegmentVerts
|
||||
.SelectMany(s => Chunk(s.Verts))
|
||||
.First(v => ColorMatches(v, UiCheckLamp.CheckedInner));
|
||||
Assert.True(checkGlyph[0] < 16f);
|
||||
|
||||
var textGlyphs = renderer.DebugSpriteSegmentVerts
|
||||
.Where(s => s.Texture == 1u)
|
||||
.SelectMany(s => Chunk(s.Verts))
|
||||
.ToList();
|
||||
Assert.Contains(textGlyphs, v => v[0] >= 224f && v[0] < 344f);
|
||||
|
||||
var icon0Quad = Assert.Single(renderer.DebugSpriteSegmentVerts, s => s.Texture == 501u);
|
||||
// Both icon columns share texture 501u/502u pairs per row, but only
|
||||
// ONE quad per texture id total across both columns (each row binds
|
||||
// the SAME Icons source to both columns) — assert its x falls in
|
||||
// EITHER icon column's range, proving the icon kind draws there and
|
||||
// nowhere else.
|
||||
Assert.True(icon0Quad.Verts[0] >= 692f && icon0Quad.Verts[0] < 724f);
|
||||
|
||||
// Click routing: one representative column per kind reaches its own
|
||||
// callback with the right row; the OTHER same-kind columns (sharing
|
||||
// a dummy callback) stay untouched.
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 8, Data2 = 18 }); // col0 (check) row1
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 260, Data2 = 0 }); // col14 (text) row0
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 700, Data2 = 18 }); // col21 (icon) row1
|
||||
|
||||
Assert.Equal(new[] { (0, 1) }, binding.Check0Fires);
|
||||
Assert.Equal(new[] { (0, 0) }, binding.Text0Fires);
|
||||
Assert.Equal(new[] { (0, 1) }, binding.Icon0Fires);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue