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;