fix(vendor): gate-findings pass — the X button HIDES like retail, clicks return, the dropdown scrolls, pyreal suffix, staged-tab slots
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The user's connected gate found five issues; each fixed at the root: G4 (the discovery): retail's vendor X button calls only SetVisible(0) (pc:204147-204182) — the SESSION stays open and re-using the vendor lands on the same-session refresh; the range watcher remains the sole real close. Our port invented a full teardown on X, which is exactly why reopening died. The Runtime fixture proves the wire dispatch was never the problem; ACE has no already-open short-circuit. G3 (regression from the drag-suppression fix): denying IsDragSource also dropped press capture, so clicks fell through to window-drag. UiItemSlot.HandlesClick now claims presses for any occupied cell independent of drag eligibility — clickable and draggable are separate concerns. G5: the authored popup 0x21000043 is ONE scrollable column with a real scrollbar subtree (live-dat scan: ListBox 0x10000350 + scrollbar 0x10000351), not a 3x6 grid. UiMenu gains an authored-driven Scrollable mode (wheel, thumb drag, track paging, up/down buttons); chat's menu is untouched and its ten tests prove it. G1: retail's cost format is "%s %hsp (you have %hsp)" — the p after each %hs is a LITERAL pyreal suffix the port swallowed as part of the specifier. Restored. G2: the Buying/Selling pages' authored lists (same cell template as Items) get the empty-slot fill, presentation-only until staging. Clean-room complete solution: 11,390 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
58c8de264e
commit
5224e43890
8 changed files with 1061 additions and 51 deletions
|
|
@ -175,4 +175,192 @@ public class UiMenuTests
|
|||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, -60)));
|
||||
Assert.Equal(ChatChannelKind.Fellowship, fired);
|
||||
}
|
||||
|
||||
// ── G5 (vendor gate finding): Scrollable single-column popup ──────────────
|
||||
// Retail's vendor category dropdown (LayoutDesc 0x21000043) is a SCROLLABLE
|
||||
// single column with a docked scrollbar, not a column-major grid — see
|
||||
// VendorUiController's "G5 correction" class-doc paragraph. Chat's own popup
|
||||
// (exercised by every test above, Scrollable left at its false default) is
|
||||
// completely unaffected: it's a structurally different code path
|
||||
// (DrawGridPopup / the grid branch of OnEvent's MouseDown handling).
|
||||
|
||||
private const int Border = 5; // RetailChromeSprites.Border
|
||||
|
||||
private static UiMenu.MenuItem[] MakeCategoryItems(int count)
|
||||
=> System.Linq.Enumerable.Range(0, count)
|
||||
.Select(i => new UiMenu.MenuItem($"Category {i}", (object?)i))
|
||||
.ToArray();
|
||||
|
||||
private static UiMenu MakeScrollableMenu(int itemCount = 18) => new UiMenu
|
||||
{
|
||||
Width = 100f, Height = 18f,
|
||||
Items = MakeCategoryItems(itemCount),
|
||||
Selected = (object?)0,
|
||||
Scrollable = true,
|
||||
RowsPerColumn = 6,
|
||||
RowHeight = 18f,
|
||||
ColumnWidth = 100f,
|
||||
ScrollbarWidth = 16f,
|
||||
ScrollButtonExtent = 16f,
|
||||
};
|
||||
|
||||
/// <summary>Raw event Data2 (the same "ly" MouseDown/MouseMove receive) for a
|
||||
/// point at popup-interior-local Y <paramref name="iy"/> — derives the mapping
|
||||
/// from the SAME public geometry properties production code reads, mirroring
|
||||
/// how CategoryMenu_OpensAndSelectsThroughRealHitPath (VendorUiControllerTests)
|
||||
/// derives its click point rather than hardcoding a pixel constant.</summary>
|
||||
private static int RawY(UiMenu menu, float iy)
|
||||
{
|
||||
float outerH = menu.RowsPerColumn * menu.RowHeight + 2 * Border;
|
||||
return (int)(iy - outerH + Border);
|
||||
}
|
||||
|
||||
/// <summary>Raw event Data1 ("lx") for a point at popup-interior-local X <paramref name="ix"/>.</summary>
|
||||
private static int RawX(float ix) => (int)(ix + Border);
|
||||
|
||||
[Fact]
|
||||
public void Scrollable_18Categories_ConfiguresScrollExtentsFromAuthoredGeometry()
|
||||
{
|
||||
var menu = MakeScrollableMenu(18);
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5)); // open
|
||||
|
||||
// A wheel-scroll (no actual movement, Data0=0) is enough to configure
|
||||
// PopupScroll from Items.Count/RowsPerColumn/RowHeight — the same
|
||||
// "configure right before use" pattern UiItemList.LayoutCells follows.
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.Scroll, Data0: 0));
|
||||
|
||||
Assert.Equal(18 * 18, menu.PopupScroll.ContentHeight); // 18 items * 18px row height
|
||||
Assert.Equal(6 * 18, menu.PopupScroll.ViewHeight); // 6 visible rows (the authored window)
|
||||
Assert.True(menu.PopupScroll.HasOverflow); // 18 > 6 -> scrollbar warranted
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scrollable_ClickInFirstVisibleRow_SelectsItemZero_ThroughTheRealHitPath()
|
||||
{
|
||||
var menu = MakeScrollableMenu(18);
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5)); // open
|
||||
object? fired = null;
|
||||
menu.OnSelect = p => fired = p;
|
||||
|
||||
// Row 0's vertical center — same "row * RowHeight + RowHeight/2" shape
|
||||
// the existing grid-mode tests already use for their row math.
|
||||
int ly = RawY(menu, menu.RowHeight / 2f);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, RawX(10), ly)));
|
||||
|
||||
Assert.Equal(0, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scrollable_ClickInScrollbarColumn_DoesNotSelectAnItem_AndKeepsThePopupOpen()
|
||||
{
|
||||
// Before G5's fix, a click at this X (where the OLD grid math would have
|
||||
// treated it as "column 1") could have picked the WRONG item entirely —
|
||||
// this X now belongs to the scrollbar, which must never fire OnSelect.
|
||||
var menu = MakeScrollableMenu(18);
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5)); // open
|
||||
var fired = new List<object?>();
|
||||
menu.OnSelect = p => fired.Add(p);
|
||||
|
||||
// Middle of the scrollbar TRACK (below the up-button, above the down-button).
|
||||
int scrollbarMidX = RawX(menu.ColumnWidth + menu.ScrollbarWidth / 2f);
|
||||
int trackMidY = RawY(menu, menu.RowsPerColumn * menu.RowHeight / 2f);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, scrollbarMidX, trackMidY)));
|
||||
|
||||
Assert.Empty(fired); // scrollbar click never selects an item
|
||||
|
||||
// The popup stayed open — a subsequent item-column click still resolves
|
||||
// (against whatever row is now visible after the scrollbar's page-scroll).
|
||||
int rowLy = RawY(menu, menu.RowHeight / 2f);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, RawX(10), rowLy)));
|
||||
Assert.Single(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scrollable_DownButtonClick_AdvancesByOneRow_AndSubsequentClickPicksTheAdvancedItem()
|
||||
{
|
||||
var menu = MakeScrollableMenu(18);
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5)); // open
|
||||
|
||||
// Down-button region: the bottom ScrollButtonExtent px of the scrollbar column.
|
||||
int downX = RawX(menu.ColumnWidth + menu.ScrollbarWidth / 2f);
|
||||
int downY = RawY(menu, menu.RowsPerColumn * menu.RowHeight - menu.ScrollButtonExtent / 2f);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, downX, downY)));
|
||||
|
||||
Assert.Equal((int)menu.RowHeight, menu.PopupScroll.ScrollY); // scrolled exactly one row
|
||||
|
||||
object? fired = null;
|
||||
menu.OnSelect = p => fired = p;
|
||||
// Row 0's ON-SCREEN position now shows item index 1 (the window advanced).
|
||||
int ly = RawY(menu, menu.RowHeight / 2f);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, RawX(10), ly)));
|
||||
Assert.Equal(1, fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scrollable_UpButtonClick_ReversesAPriorDownScroll()
|
||||
{
|
||||
var menu = MakeScrollableMenu(18);
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5));
|
||||
|
||||
int scrollbarMidX = RawX(menu.ColumnWidth + menu.ScrollbarWidth / 2f);
|
||||
int downY = RawY(menu, menu.RowsPerColumn * menu.RowHeight - menu.ScrollButtonExtent / 2f);
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, scrollbarMidX, downY));
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, scrollbarMidX, downY));
|
||||
Assert.Equal((int)(2 * menu.RowHeight), menu.PopupScroll.ScrollY);
|
||||
|
||||
int upY = RawY(menu, menu.ScrollButtonExtent / 2f);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, scrollbarMidX, upY)));
|
||||
|
||||
Assert.Equal((int)menu.RowHeight, menu.PopupScroll.ScrollY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scrollable_MouseWheel_ScrollsWhilePopupIsOpen()
|
||||
{
|
||||
var menu = MakeScrollableMenu(18);
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5)); // open
|
||||
|
||||
// Mirrors UiItemList's own wheel convention: +Y wheel (Data0>0) scrolls up/older.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.Scroll, Data0: -1)));
|
||||
|
||||
Assert.Equal((int)menu.RowHeight, menu.PopupScroll.ScrollY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scrollable_ThumbDrag_MovesScrollPosition_AndReleaseKeepsThePopupOpen()
|
||||
{
|
||||
var menu = MakeScrollableMenu(18);
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, 10, 5)); // open
|
||||
|
||||
// Establish the thumb's starting rect the SAME way production drawing/
|
||||
// hit-testing does: configure the model, then ask UiScrollbar's own
|
||||
// shared geometry helper (the exact function DrawPopupScrollbar/
|
||||
// HandleScrollablePopupMouseDown use internally).
|
||||
int trackTopY = (int)menu.ScrollButtonExtent;
|
||||
float trackLen = menu.RowsPerColumn * menu.RowHeight - 2 * menu.ScrollButtonExtent;
|
||||
// Force PopupScroll into a known-configured state via a zero-delta wheel
|
||||
// event before computing the thumb rect from it.
|
||||
menu.OnEvent(new UiEvent(0, menu, UiEventType.Scroll, Data0: 0));
|
||||
var (thumbY, thumbH) = UiScrollbar.ThumbRect(menu.PopupScroll, trackTopY, trackLen);
|
||||
|
||||
int scrollbarMidX = RawX(menu.ColumnWidth + menu.ScrollbarWidth / 2f);
|
||||
int pressY = RawY(menu, thumbY + thumbH / 2f); // press inside the thumb
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, scrollbarMidX, pressY)));
|
||||
|
||||
// Drag most of the way down the track.
|
||||
int dragToIy = (int)(trackTopY + trackLen - thumbH / 2f);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseMove, 0, scrollbarMidX, RawY(menu, dragToIy))));
|
||||
|
||||
Assert.True(menu.PopupScroll.ScrollY > 0);
|
||||
Assert.True(menu.PopupScroll.PositionRatio > 0.5f);
|
||||
|
||||
// Releasing must NOT close the popup — a subsequent scrollbar/item click
|
||||
// still resolves through the same OnEvent path.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseUp, 0, scrollbarMidX, RawY(menu, dragToIy))));
|
||||
object? fired = null;
|
||||
menu.OnSelect = p => fired = p;
|
||||
int ly = RawY(menu, menu.RowHeight / 2f);
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, 0, RawX(10), ly)));
|
||||
Assert.NotNull(fired); // popup was still open -> the click landed on a real row
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue