Files
PRA-only-demo/packages/rpa-core/test/capabilities/desktop-uia.test.ts
T

166 lines
6.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const powershellMock = vi.hoisted(() => vi.fn(async () => '{"clicked":true,"value":"cached"}'));
vi.mock("../driver/windows/powershell", () => ({
powershell: powershellMock,
psString: (value: unknown) => JSON.stringify(String(value ?? ""))
}));
import { getHandler } from "../../src/engine/registry";
import { parseUiaSelector } from "../../src/capabilities/desktop-uia";
describe("desktop UIA ground capabilities", () => {
it.each([
"Element_ClickGround",
"Element_SetValueGround",
"Element_ExistsGround",
"Element_GetValueGround",
"Element_WaitElementGround",
"Element_GetCheckGround",
"Element_SetCheckGround",
"Element_RegionGround",
"Element_GetSelectGround",
"Element_SetSelectGround",
"Element_GetAttributeGround",
"Element_SetAttributeGround",
"Element_GetButtonEnableGround",
"Element_ShotGround",
"Element_MouseToGround",
"Element_ScrollIntoViewGround",
"Element_ChildrenGround",
"Element_DesktopChildren",
"Element_RestoreWindowGround",
"Element_ActivateWindowGround",
"Element_MoveWindowGround"
])("registers %s", (id) => {
expect(getHandler(id), id).toBeTruthy();
});
it("parses name selector", () => {
const s = parseUiaSelector("name=添加供应商");
expect(s.name).toBe("添加供应商");
expect(s.index).toBe(1);
});
it("parses window, id, type, class and index", () => {
const s = parseUiaSelector("window=CC Switch;id=btn-save;type=button;class=btn;index=2");
expect(s.window).toBe("CC Switch");
expect(s.id).toBe("btn-save");
expect(s.type).toBe("Button");
expect(s.className).toBe("btn");
expect(s.index).toBe(2);
});
it("strips uia: prefix and accepts object form", () => {
const s = parseUiaSelector({ value: "uia:name=确定" });
expect(s.name).toBe("确定");
});
it("parses AI screen coordinate selector", () => {
const s = parseUiaSelector("screen=100,200,320,80;window= MyApp;ai=1");
expect(s.screen).toEqual({ x: 100, y: 200, w: 320, h: 80 });
expect(s.window).toBe("MyApp");
});
it("parses enriched AI relocation metadata", () => {
const selector = parseUiaSelector(
"screen=100,200,320,80;window=App;winClass=Chrome_WidgetWin_1;process=chrome;rel=20,30,320,80;name=login;id=loginButton;type=Button;class=Button;aiType=button;aiText=login;enriched=1;ai=1"
);
expect(selector.screen).toEqual({ x: 100, y: 200, w: 320, h: 80 });
expect(selector.rel).toEqual({ x: 20, y: 30, w: 320, h: 80 });
expect(selector.window).toBe("App");
expect(selector.winClass).toBe("Chrome_WidgetWin_1");
expect(selector.process).toBe("chrome");
expect(selector).toMatchObject({ name: "login", id: "loginButton", type: "Button", className: "Button", aiType: "button", aiText: "login", enriched: true, ai: true });
});
it("rejects invalid selector", () => {
expect(() => parseUiaSelector("")).toThrow();
expect(() => parseUiaSelector("screen=1,2,3")).toThrow();
});
});
describe("desktop UIA selector compatibility", () => {
beforeEach(() => {
powershellMock.mockClear();
powershellMock.mockImplementation(async () => '{"clicked":true,"value":"cached"}');
});
it("maps compound .NET ControlType names explicitly and preserves raw input", () => {
expect(parseUiaSelector("type=combobox")).toMatchObject({ type: "ComboBox", typeRaw: "combobox" });
expect(parseUiaSelector("type=listitem")).toMatchObject({ type: "ListItem", typeRaw: "listitem" });
expect(parseUiaSelector("type=datagrid")).toMatchObject({ type: "DataGrid", typeRaw: "datagrid" });
});
it("parses a stable ancestor path", () => {
const selector = parseUiaSelector("window=App;path=Pane>Edit[name=用户名;index=2]>Button[name=确定]");
expect(selector.path).toEqual([
{ type: "Pane" },
{ type: "Edit", name: "用户名", index: 2 },
{ type: "Button", name: "确定" }
]);
});
const context = { log: vi.fn() } as any;
const node = { id: "n1", componentId: "Element_ClickGround" } as any;
it("resolves screen selectors through UIA/window before coordinate fallback", async () => {
const args = {
selector: "screen=100,200,320,80;ai=1",
clickway: "1"
};
await expect(getHandler("Element_ClickGround")!(context, node, args, {} as any)).resolves.toEqual({ clicked: true, value: "cached" });
const script = String((powershellMock.mock.calls as unknown as Array<[string]>)[0]?.[0] ?? "");
expect(script).toContain("RpaScreenInput");
expect(script).toContain("$x = [int]($rect.x + $rect.w / 2)");
expect(script).toContain("SetCursorPos($x, $y)");
expect(script).toContain("Resolve-RpaScreenRect");
expect(script).toContain("Find-RpaWindow");
});
it("元素定位到但 clicked=false 时必须抛错,不允许静默成功(用户 NVIDIA 覆盖层踩坑场景)", async () => {
powershellMock.mockImplementation(async () => '{"clicked":false,"method":"invoke_pattern","name":"","x":0,"y":0,"w":1919,"h":1080}');
let error: any = null;
try {
await getHandler("Element_ClickGround")!(
context,
node,
{ selector: "window=NVIDIA GeForce Overlay DT;type=Document;id=RootWebArea" },
{} as any
);
} catch (e) {
error = e;
}
expect(error?.code).toBe("desktop_error");
// recommendation 必须给出可执行建议
expect(String(error?.recommendation ?? "")).toContain("桌面拾取");
});
it("dispatches screen setValue to coordinate paste", async () => {
await getHandler("Element_SetValueGround")!(context, node, {
selector: "screen=10,20,30,40",
value: "hello"
}, {} as any);
const script = String((powershellMock.mock.calls as unknown as Array<[string]>)[0]?.[0] ?? "");
expect(script).toContain("resolvedMethod = $rect.method");
expect(script).toContain("hello");
expect(script).toContain("Find-RpaWindow");
});
it("validates a picked screen rectangle for exists/wait", async () => {
powershellMock.mockImplementation(async () => '{"found":true,"method":"window_rel"}');
await expect(getHandler("Element_ExistsGround")!(context, node, {
selector: "screen=1,2,3,4"
}, {} as any)).resolves.toBe(true);
await expect(getHandler("Element_WaitElementGround")!(context, node, {
selector: "screen=1,2,3,4"
}, {} as any)).resolves.toBe(true);
expect(powershellMock).toHaveBeenCalled();
const script = String((powershellMock.mock.calls as unknown as Array<[string]>).at(-1)?.[0] ?? "");
expect(script).toContain("Resolve-RpaScreenRect");
});
});