acdream/tests/AcDream.App.Tests/UI/UiTemplateListBoxViewportTests.cs

173 lines
8.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AcDream.App.UI;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI;
/// <summary>
/// #372 regression: the Options panel's Character/Chat/Config tabs rendered
/// BLANK at the first connected gate while Gameplay (which has no ListBox)
/// worked. Root cause: <see cref="UiTemplateListBox"/> creates its row
/// viewport at 0×0 with Left|Top|Right|Bottom fill-anchors; the anchor
/// baseline captured from that 0-size rect makes
/// <c>ComputeAnchoredRect</c> keep it 0×0 forever (mR=parentW, so
/// w=parentW-mR-mL=0), and <see cref="UiScrollablePanel.LayoutScrollableChildren"/>
/// then culls every row (top+height ≤ 0 is false). These tests pin that the
/// viewport fills its ListBox and the rows stay visible after a layout pass.
/// </summary>
public sealed class UiTemplateListBoxViewportTests
{
private static UiTemplateListBox MakeListBox(float width, float height)
{
// A ListBox element authoring one template entry, sized like the
// Options-panel Character ListBox (0x100001FA, 276×560 authored).
var info = new ElementInfo { Id = 0x100001FAu, Type = 5u };
var box = new UiTemplateListBox(
info,
_ => (0u, 0, 0),
new[] { new UiTemplateListEntry(0x2100002Bu, 0x10000218u) },
scrollbarElementId: 0x100001FBu)
{
Width = width,
Height = height,
};
return box;
}
[Fact]
public void Viewport_FillsTheListBox_NotCollapsedToZero()
{
var box = MakeListBox(276f, 560f);
// Add a couple of rows via the prebuilt path (no DAT needed).
box.AddPrebuiltRow(new UiText { Width = 260f, Height = 20f });
box.AddPrebuiltRow(new UiText { Width = 260f, Height = 20f });
UiScrollablePanel? viewport = box.ViewportForTest;
Assert.NotNull(viewport);
// Drive the per-frame layout the draw traversal runs: the parent
// ListBox re-anchors its children (UiElement.DrawSelfAndChildren
// line ~500), sizing the fill-anchored viewport.
viewport!.ApplyAnchor(box.Width, box.Height);
// The whole point of #372: the viewport must FILL the ListBox, not
// collapse. Pre-fix this was 0×0.
Assert.Equal(276f, viewport.Width, 3);
Assert.Equal(560f, viewport.Height, 3);
}
[Fact]
public void Rows_StayVisible_AfterLayout_WhenTheyFitTheViewport()
{
var box = MakeListBox(276f, 560f);
var row0 = new UiText { Width = 260f, Height = 20f };
var row1 = new UiText { Width = 260f, Height = 20f };
var row2 = new UiText { Width = 260f, Height = 20f };
box.AddPrebuiltRow(row0);
box.AddPrebuiltRow(row1);
box.AddPrebuiltRow(row2);
UiScrollablePanel viewport = box.ViewportForTest!;
viewport.ApplyAnchor(box.Width, box.Height); // size the viewport
viewport.LayoutScrollableChildren(); // the cull pass (runs each OnDraw)
// All three rows sit at y 0/20/40 inside a 560px viewport → visible.
// Pre-fix the viewport was 0px tall and every row was culled → blank tab.
Assert.True(row0.Visible, "row 0 culled — the #372 blank-tab bug");
Assert.True(row1.Visible, "row 1 culled — the #372 blank-tab bug");
Assert.True(row2.Visible, "row 2 culled — the #372 blank-tab bug");
}
[Fact]
public void RemoveTail_PreservesPrefixAndRestacksFutureRows()
{
var box = MakeListBox(276f, 560f);
var prefix = new UiText { Width = 260f, Height = 20f };
var removedA = new UiText { Width = 260f, Height = 30f };
var removedB = new UiText { Width = 260f, Height = 40f };
box.AddPrebuiltRow(prefix);
box.AddPrebuiltRow(removedA);
box.AddPrebuiltRow(removedB);
box.RemoveTail(1);
var replacement = new UiText { Width = 260f, Height = 25f };
box.AddPrebuiltRow(replacement);
Assert.Equal(2, box.ItemCount);
Assert.Same(prefix, box.ViewportForTest!.Children[0]);
Assert.Same(replacement, box.ViewportForTest.Children[1]);
Assert.Equal(20f, replacement.Top);
Assert.Null(removedA.Parent);
Assert.Null(removedB.Parent);
}
/// <summary>
/// #412-class regression (2026-08-16, overnight hover/UI round, Batch A bug
/// 2): the Options panel's Config tab escaped past the window frame — the
/// footer sitting mid-panel with further rows drawing below the window's
/// bottom edge. #372's own fixture above never exercises this because
/// <see cref="MakeListBox"/> gives the ListBox no parent — its own size
/// never changes after the viewport is seeded. The real Options mount is
/// different: this ListBox is itself a DAT-imported <see cref="UiElement"/>
/// carrying a real <see cref="UiLayoutPolicy"/> from its authored parent
/// (the Config page slot), and a page controller's Bind — which lazily
/// creates this viewport — runs BEFORE the tree's first draw frame, i.e.
/// before the ListBox has ever shrunk to fit its actual (smaller than
/// authored) container. This reproduces that ordering with a real
/// LayoutPolicy-driven parent standing in for the page slot.
/// </summary>
[Fact]
public void Viewport_TracksTheListBox_WhenTheListBoxItselfShrinksOnFirstLayout()
{
// A stand-in for the Config page slot: authored 298×575 against an
// authored 300×600 design canvas (live-DAT-measured values), but its
// real mounted container is only 300×362 — exactly retail's
// UIElement::UpdateForParentSizeChange four-edge policy (L=T=R=B=1,
// "preserve original margin on every edge").
var slotPolicy = new UiLayoutPolicy(
leftMode: 1, topMode: 1, rightMode: 1, bottomMode: 1,
originalChild: UiPixelRect.FromPositionAndSize(2, 25, 298, 575),
originalParent: UiPixelRect.FromPositionAndSize(0, 0, 300, 600));
var slot = new UiPanel
{
Left = 2, Top = 25, Width = 298, Height = 575,
LayoutPolicy = slotPolicy,
};
var root = new UiPanel { Width = 300, Height = 362 };
root.AddChild(slot);
// The ListBox itself ALSO carries a real LayoutPolicy (live-DAT
// measured: authored 276×560 against the slot's own 298×575 design
// extent) — this is what shrinks it out from under the viewport.
var box = MakeListBox(276f, 560f);
var listBoxPolicy = new UiLayoutPolicy(
leftMode: 1, topMode: 1, rightMode: 1, bottomMode: 1,
originalChild: UiPixelRect.FromPositionAndSize(0, 0, 276, 560),
originalParent: UiPixelRect.FromPositionAndSize(0, 0, 298, 575));
box.LayoutPolicy = listBoxPolicy;
slot.AddChild(box);
// Seed the viewport with rows BEFORE any layout pass has ever run —
// exactly ConfigOptionsPageController.Bind's own ordering (it runs
// before RetailWindowFrame.Mount's first draw frame).
box.AddPrebuiltRow(new UiText { Width = 260f, Height = 20f });
UiScrollablePanel viewport = box.ViewportForTest!;
// Drive ONE simulated draw-frame's top-down ApplyAnchor walk — the
// SAME order DrawSelfAndChildren runs every frame: parent before
// children, all the way down.
slot.ApplyAnchor(root.Width, root.Height); // slot shrinks: 575 -> ~337
box.ApplyAnchor(slot.Width, slot.Height); // listbox shrinks: 560 -> ~297 (still ahead of the viewport)
viewport.ApplyAnchor(box.Width, box.Height); // the viewport's FIRST EVER ApplyAnchor call
// Pre-fix: the viewport's legacy anchor baseline captured a NEGATIVE
// bottom margin against the ALREADY-SHRUNK ListBox (parentH(~297) -
// (0+560) < 0), which ComputeAnchoredRect's stretch math preserves
// forever — the viewport stayed locked at its original 560px height,
// clipping rows to a bound retail never actually gave it on screen.
Assert.Equal(box.Height, viewport.Height, 3);
Assert.True(
viewport.Height < 400f,
$"viewport height {viewport.Height} did not shrink with its ListBox "
+ "(560 == the pre-fix stale-capture bug)");
}
}