import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; import type { CatalogComponent, FlowNode } from "../../shared/types"; import { getHandler } from "@rpa/core"; import type { RunContext } from "@rpa/core"; import "./window-extended"; const run = promisify(execFile); const component = {} as CatalogComponent; const node = (id: string): FlowNode => ({ id: "smoke-mouse", componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} }); const ctx = { runId: "mouse-smoke", flowId: "mouse-smoke-flow", flow: {} as RunContext["flow"], vars: {}, secrets: {}, signal: new AbortController().signal, evidenceDir: "D:/office-test2", emit: () => {}, log: () => {}, componentOf: () => undefined } as unknown as RunContext; const PS_API = `Add-Type @' using System; using System.Runtime.InteropServices; public static class RpaMouseSmoke { [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; } [DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p); [DllImport("user32.dll")] public static extern bool SetProcessDpiAwarenessContext(IntPtr v); } '@; try { [void][RpaMouseSmoke]::SetProcessDpiAwarenessContext([IntPtr](-4)) } catch {}; $c = [RpaMouseSmoke+POINT]::new(); [void][RpaMouseSmoke]::GetCursorPos([ref]$c); "$($c.X),$($c.Y)"`; async function cursorPos(): Promise<[number, number]> { const { stdout } = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_API], { windowsHide: true }); return stdout.trim().split(",").map(Number) as [number, number]; } /** 共享桌面环境下光标可能被其他进程短暂挪动,轮询等待落位(容差 px),超时返回最后一次读数 */ async function waitForCursor(target: [number, number], tolerance = 0, attempts = 10, gapMs = 100): Promise<[number, number]> { let pos = await cursorPos(); for (let i = 0; i < attempts; i += 1) { if (Math.abs(pos[0] - target[0]) <= tolerance && Math.abs(pos[1] - target[1]) <= tolerance) return pos; await new Promise((r) => setTimeout(r, gapMs)); pos = await cursorPos(); } return pos; } async function invoke(componentId: string, args: Record): Promise { const handler = getHandler(componentId); expect(handler).toBeTypeOf("function"); return handler!(ctx, node(componentId), args, component); } let original: [number, number]; afterEach(async () => { if (original) await invoke("Mouse_MoveToV2", { location: { x: original[0], y: original[1] }, position: "0", move_type: "2", speed: 100 }); }); describe("移动鼠标组件真实冒烟", () => { it("对象坐标 x/y JSON 不再报无效, 瞬间移动真实落位", async () => { original = await cursorPos(); // 用户屏幕框选生成的浮点对象坐标(此前直接报「鼠标坐标无效」) const out = await invoke("Mouse_MoveToV2", { location: { x: 1397, y: 227.60000610351562 }, position: "0", move_type: "2", speed: 100 }) as { x: number; y: number }; expect(out).toEqual({ x: 1397, y: 228 }); const pos = await waitForCursor([1397, 228]); expect(pos[0]).toBe(1397); expect(pos[1]).toBe(228); console.log("[移动鼠标] 对象坐标解析 + 瞬间移动落位 ✓ 实际光标:", pos); }, 60000); it("模拟人工移动(默认方式)平滑到达目标", async () => { original = await cursorPos(); const out = await invoke("Mouse_MoveToV2", { location: "[300,300]", position: "0", move_type: "0", speed: 60 }) as { x: number; y: number }; expect(out).toEqual({ x: 300, y: 300 }); const pos = await waitForCursor([300, 300], 2); expect(Math.abs(pos[0] - 300)).toBeLessThanOrEqual(2); expect(Math.abs(pos[1] - 300)).toBeLessThanOrEqual(2); console.log("[移动鼠标] 模拟人工移动 ✓ 实际光标:", pos); }, 60000); it("匀速直线移动与字符串数组坐标", async () => { original = await cursorPos(); const out = await invoke("Mouse_MoveToV2", { location: "[500,400]", position: "0", move_type: "1", speed: 80 }) as { x: number; y: number }; expect(out).toEqual({ x: 500, y: 400 }); const pos = await waitForCursor([500, 400]); expect(pos[0]).toBe(500); expect(pos[1]).toBe(400); console.log("[移动鼠标] 匀速直线 + 字符串坐标 ✓ 实际光标:", pos); }, 60000); it("相对激活窗口左上角模式返回有效坐标", async () => { original = await cursorPos(); const out = await invoke("Mouse_MoveToV2", { location: { x: 10, y: 10 }, position: "1", move_type: "2", speed: 100 }) as { x: number; y: number }; expect(Number.isFinite(out.x)).toBe(true); expect(Number.isFinite(out.y)).toBe(true); console.log("[移动鼠标] 相对激活窗口模式 ✓ 目标:", out); }, 60000); });