126 lines
6.2 KiB
TypeScript
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" });
|
|
});
|
|
});
|