test(ui): automation runner gains hover/mousemove — synthetic pointer verify without the OS cursor
Morning gate live-verify apparatus: 'hover element <datId>' and 'mousemove <x> <y>' drive UiRoot.OnMouseMove synthetically (no click, no real cursor theft — a user is present at the machine during this round, unlike the overnight rounds whose drive scripts moved the physical cursor). Deliberately no probe-clock Advance: hover dwell and the world-tooltip timing must ride the production frame tick's real monotonic clock, which keeps running between script commands — an Advance would stamp the idle timestamp with the probe's tiny private counter. (Committed from a re-attached worktree: the round's original worktree was pruned from git's registry mid-session by an external cleanup; the branch and all three finding commits were unaffected.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
942a02af11
commit
302d90209d
2 changed files with 56 additions and 0 deletions
|
|
@ -131,6 +131,32 @@ public sealed class RetailUiAutomationProbe
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 2026-08-17 morning gate: synthetic pointer HOVER (no click) at an
|
||||||
|
/// element's center, for rollover/tooltip verification. Deliberately
|
||||||
|
/// does NOT <see cref="Advance"/> the probe's own micro-clock: hover
|
||||||
|
/// dwell (<see cref="UiRoot.MouseIdleMs"/> vs
|
||||||
|
/// <see cref="UiRoot.TooltipDelayMs"/>) and the world-tooltip timing
|
||||||
|
/// must ride the PRODUCTION frame tick's real monotonic clock
|
||||||
|
/// (<c>UiHost.Tick</c>'s <c>TickCount64</c>), which keeps running
|
||||||
|
/// between script commands — an <c>Advance</c> 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Raw synthetic mouse move (canvas coordinates). Same
|
||||||
|
/// no-<see cref="Advance"/> contract as <see cref="HoverElement"/> —
|
||||||
|
/// used to sweep the pointer across the 3D world for the world-hover
|
||||||
|
/// tooltip's mouse-idle dwell verification.</summary>
|
||||||
|
public void MoveMouse(int x, int y) => _root.OnMouseMove(x, y);
|
||||||
|
|
||||||
public bool ClickItem(uint itemGuid, ItemDragSource? sourceKind = null)
|
public bool ClickItem(uint itemGuid, ItemDragSource? sourceKind = null)
|
||||||
{
|
{
|
||||||
var target = FindByItemId(itemGuid, sourceKind);
|
var target = FindByItemId(itemGuid, sourceKind);
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,8 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
|
||||||
{
|
{
|
||||||
"dump" => DoDump(),
|
"dump" => DoDump(),
|
||||||
"click" => DoClick(command),
|
"click" => DoClick(command),
|
||||||
|
"hover" => DoHover(command),
|
||||||
|
"mousemove" => DoMouseMove(command),
|
||||||
"doubleclick" => DoDoubleClick(command),
|
"doubleclick" => DoDoubleClick(command),
|
||||||
"drag" => DoDrag(command),
|
"drag" => DoDrag(command),
|
||||||
"wait" => DoWait(command),
|
"wait" => DoWait(command),
|
||||||
|
|
@ -232,6 +234,34 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
|
||||||
return Stop(command, "usage: click element <datId> | click item <guid> [source]");
|
return Stop(command, "usage: click element <datId> | click item <guid> [source]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>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
|
||||||
|
/// <see cref="RetailUiAutomationProbe.HoverElement"/>'s own doc for the
|
||||||
|
/// deliberate no-<c>Advance</c> clock contract.</summary>
|
||||||
|
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 <datId>");
|
||||||
|
if (!TryParseUInt(p[2], out uint datId)) return Stop(command, $"bad element id '{p[2]}'");
|
||||||
|
return _probe.HoverElement(datId) || Stop(command, "hover element failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>`mousemove <x> <y>` — raw synthetic pointer move in
|
||||||
|
/// canvas coordinates (world-hover dwell sweeps).</summary>
|
||||||
|
private bool DoMouseMove(ScriptCommand command)
|
||||||
|
{
|
||||||
|
var p = command.Parts;
|
||||||
|
if (p.Length != 3)
|
||||||
|
return Stop(command, "usage: mousemove <x> <y>");
|
||||||
|
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)
|
private bool DoDoubleClick(ScriptCommand command)
|
||||||
{
|
{
|
||||||
var p = command.Parts;
|
var p = command.Parts;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue