PRA-only-demo/src/main/capabilities/browser-dom.test.ts

126 lines
6.2 KiB
TypeScript

import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import { getHandler } from "../engine/registry";
import { RuntimeError, type BrowserElementRegion, type BrowserLike, type RunContext } from "../engine/types";
import { BROWSER_DOM_IDS } from "./browser-dom";
const component = {} as CatalogComponent;
const selector = { kind: "css", value: "#target" };
const region: BrowserElementRegion = {
x: 10, y: 20, width: 100, height: 40,
left: 10, top: 20, right: 110, bottom: 60,
centerX: 60, centerY: 40, pageX: 10, pageY: 120
};
function node(componentId: string): FlowNode {
return { id: `dom_${componentId}`, componentId, position: { x: 0, y: 0 }, values: {}, options: {} };
}
function fakeBrowser(): BrowserLike {
return {
navigate: vi.fn(async (url: string) => ({ url, title: "Fake page" })),
click: vi.fn(), type: vi.fn(), getText: vi.fn(), exists: vi.fn(), select: vi.fn(), runJs: vi.fn(),
download: vi.fn(), screenshot: vi.fn(), currentUrl: vi.fn(), close: vi.fn(),
getAttribute: vi.fn(async () => "ready"),
setAttribute: vi.fn(async (_selector, name, value) => ({ name, value })),
getHtml: vi.fn(async () => '<button id="target">Run</button>'),
getChecked: vi.fn(async () => true),
setChecked: vi.fn(async (_selector, checked) => checked),
isEnabled: vi.fn(async () => true),
scrollIntoView: vi.fn(async () => region),
getRegion: vi.fn(async () => region),
hover: vi.fn(async () => region),
screenshotElement: vi.fn(async (_selector, target) => target)
};
}
function context(browser: BrowserLike): RunContext {
return {
runId: "dom-run", flowId: "dom-flow", flow: {} as RunContext["flow"], vars: {}, secrets: {},
signal: new AbortController().signal, evidenceDir: "C:/temp/evidence", browser,
emit: vi.fn(), log: vi.fn(), componentOf: vi.fn()
};
}
async function invoke(ctx: RunContext, componentId: string, args: Record<string, unknown>): Promise<unknown> {
const handler = getHandler(componentId);
expect(handler, `${componentId} should be registered`).toBeTypeOf("function");
return handler!(ctx, node(componentId), args, component);
}
describe("browser DOM component registration", () => {
it("registers real catalog IDs without Ground variants", () => {
const catalog = JSON.parse(readFileSync(resolve("resources/catalog.json"), "utf8")) as {
components: Array<{ id: string }>;
};
const catalogIds = new Set(catalog.components.map((item) => item.id));
expect(BROWSER_DOM_IDS.length).toBeGreaterThanOrEqual(12);
expect(BROWSER_DOM_IDS.some((id) => id.includes("Ground"))).toBe(false);
expect(BROWSER_DOM_IDS.filter((id) => !catalogIds.has(id))).toEqual([]);
expect(BROWSER_DOM_IDS.every((id) => typeof getHandler(id) === "function")).toBe(true);
});
});
describe("browser DOM capability routing", () => {
let browser: BrowserLike;
let ctx: RunContext;
beforeEach(() => {
browser = fakeBrowser();
ctx = context(browser);
});
it("gets and sets attributes and HTML", async () => {
await expect(invoke(ctx, "Element_GetAttribute_v3", { uiSelector: selector, attrName: "data-state" })).resolves.toBe("ready");
expect(browser.getAttribute).toHaveBeenCalledWith(selector, "data-state");
await invoke(ctx, "Element_SetAttribute_v2", { uiSelector: selector, attrName: "role", attrValue: "switch" });
expect(browser.setAttribute).toHaveBeenCalledWith(selector, "role", "switch");
await expect(invoke(ctx, "Element_GetHtml", { selector })).resolves.toContain("button");
});
it("gets, sets and operates checkbox states", async () => {
await expect(invoke(ctx, "Element_GetCheck", { uiSelector: selector })).resolves.toBe(true);
await expect(invoke(ctx, "Element_SetCheck", { uiSelector: selector, isChecked: "0" })).resolves.toBe(false);
expect(browser.setChecked).toHaveBeenCalledWith(selector, false);
await invoke(ctx, "Element_CheckBoxOperation_v3", { uiSelector: selector, operation_type: "1" });
expect(browser.setChecked).toHaveBeenLastCalledWith(selector, true);
await invoke(ctx, "Element_CheckBoxOperation_v3", { uiSelector: selector, operation_type: "2" });
expect(browser.setChecked).toHaveBeenLastCalledWith(selector, false);
await expect(invoke(ctx, "Element_CheckBoxOperation_v3", { uiSelector: selector, operation_type: "3" })).resolves.toBe(true);
});
it("checks enabled state and honors hidden-element mode", async () => {
await expect(invoke(ctx, "Element_GetButtonEnable_v3", {
uiSelector: selector, identify_hidden_element: "1"
})).resolves.toBe(true);
expect(browser.isEnabled).toHaveBeenCalledWith(selector, true);
});
it("scrolls, reads regions and hovers", async () => {
await expect(invoke(ctx, "Element_ScrollIntoView_v2", { uiSelector: selector })).resolves.toEqual(region);
await expect(invoke(ctx, "Element_Region2_v3", { uiSelector: selector, position: "3" })).resolves.toEqual({ ...region, position: "3" });
await expect(invoke(ctx, "Element_MouseTo_V4", { uiSelector: selector })).resolves.toEqual(region);
expect(browser.scrollIntoView).toHaveBeenCalledWith(selector);
expect(browser.getRegion).toHaveBeenCalledWith(selector);
expect(browser.hover).toHaveBeenCalledWith(selector);
});
it("captures element screenshots with viewport semantics", async () => {
const result = await invoke(ctx, "Element_Shot_V2", {
uiSelector: selector, filePath: "C:/temp/evidence", filename: "target.png", isUseVerifyCode: "1"
});
expect(result).toMatch(/target\.png$/);
expect(browser.screenshotElement).toHaveBeenCalledWith(selector, expect.stringMatching(/target\.png$/), false);
});
it("rejects missing selectors, attributes and designated browser objects", async () => {
await expect(invoke(ctx, "Element_GetHtml", {})).rejects.toBeInstanceOf(RuntimeError);
await expect(invoke(ctx, "Element_GetAttribute", { uiSelector: selector })).rejects.toBeInstanceOf(RuntimeError);
await expect(invoke(ctx, "Element_SetAttribute_v2", {
uiSelector: selector, attrName: "role", attrValue: "button", designated_info: "1"
})).rejects.toMatchObject({ code: "validation_error" });
});
});