diff --git a/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs b/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs index 42218789..d3ab64c1 100644 --- a/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs +++ b/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs @@ -131,6 +131,32 @@ public sealed class RetailUiAutomationProbe return true; } + /// + /// 2026-08-17 morning gate: synthetic pointer HOVER (no click) at an + /// element's center, for rollover/tooltip verification. Deliberately + /// does NOT the probe's own micro-clock: hover + /// dwell ( vs + /// ) and the world-tooltip timing + /// must ride the PRODUCTION frame tick's real monotonic clock + /// (UiHost.Tick's TickCount64), which keeps running + /// between script commands — an Advance here would stamp the + /// idle timestamp with this probe's tiny private counter and make the + /// next production tick read an enormous (or negative) idle time. + /// + public bool HoverElement(uint datElementId) + { + var target = FindByDatElementId(datElementId); + if (target is null) return Fail($"element 0x{datElementId:X8} not found"); + _root.OnMouseMove((int)target.CenterX, (int)target.CenterY); + return true; + } + + /// Raw synthetic mouse move (canvas coordinates). Same + /// no- contract as — + /// used to sweep the pointer across the 3D world for the world-hover + /// tooltip's mouse-idle dwell verification. + public void MoveMouse(int x, int y) => _root.OnMouseMove(x, y); + public bool ClickItem(uint itemGuid, ItemDragSource? sourceKind = null) { var target = FindByItemId(itemGuid, sourceKind); diff --git a/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs b/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs index 4fc0e1f7..46d0ec37 100644 --- a/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs +++ b/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs @@ -195,6 +195,8 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable { "dump" => DoDump(), "click" => DoClick(command), + "hover" => DoHover(command), + "mousemove" => DoMouseMove(command), "doubleclick" => DoDoubleClick(command), "drag" => DoDrag(command), "wait" => DoWait(command), @@ -232,6 +234,34 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable return Stop(command, "usage: click element | click item [source]"); } + /// 2026-08-17 morning gate: `hover element <datId>` — a + /// synthetic pointer hover with NO click, for rollover-state and + /// tooltip-dwell verification (the drive scripts must not move the real + /// OS cursor when a user is present). See + /// 's own doc for the + /// deliberate no-Advance clock contract. + private bool DoHover(ScriptCommand command) + { + var p = command.Parts; + if (p.Length < 3 || !string.Equals(p[1], "element", StringComparison.OrdinalIgnoreCase)) + return Stop(command, "usage: hover element "); + if (!TryParseUInt(p[2], out uint datId)) return Stop(command, $"bad element id '{p[2]}'"); + return _probe.HoverElement(datId) || Stop(command, "hover element failed"); + } + + /// `mousemove <x> <y>` — raw synthetic pointer move in + /// canvas coordinates (world-hover dwell sweeps). + private bool DoMouseMove(ScriptCommand command) + { + var p = command.Parts; + if (p.Length != 3) + return Stop(command, "usage: mousemove "); + if (!TryParseInt(p[1], out int x)) return Stop(command, $"bad x '{p[1]}'"); + if (!TryParseInt(p[2], out int y)) return Stop(command, $"bad y '{p[2]}'"); + _probe.MoveMouse(x, y); + return true; + } + private bool DoDoubleClick(ScriptCommand command) { var p = command.Parts;