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 () => ''), 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): Promise { 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" }); }); });