mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
1037 lines
31 KiB
TypeScript
1037 lines
31 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { renderVom } from "../render";
|
|
import type { VomNode, VomScene } from "../types";
|
|
|
|
function node(p: Partial<VomNode> & { id: number }): VomNode {
|
|
const { id, parentId, tag, rect, paintOrder, position, pointerEvents, ...rest } = p;
|
|
|
|
return {
|
|
id,
|
|
parentId: parentId ?? null,
|
|
tag: tag ?? "div",
|
|
rect: rect ?? null,
|
|
paintOrder: paintOrder ?? 0,
|
|
position: position ?? "static",
|
|
pointerEvents: pointerEvents ?? "auto",
|
|
...rest,
|
|
};
|
|
}
|
|
|
|
function scene(nodes: VomNode[]): VomScene {
|
|
return {
|
|
viewport: { width: 1280, height: 720 },
|
|
nodes,
|
|
};
|
|
}
|
|
|
|
function coreRefs(refs: ReturnType<typeof renderVom>["refs"]) {
|
|
return refs.map(({ ref, backendNodeId }) => ({ ref, backendNodeId }));
|
|
}
|
|
|
|
describe("renderVom single-layer page", () => {
|
|
it("renders visual surfaces as screenshot-only refs", () => {
|
|
const input = scene([
|
|
node({ id: 1, role: "RootWebArea", frameId: "main" }),
|
|
node({ id: 2, parentId: 1, role: "Iframe", frameId: "main", backendNodeId: 20 }),
|
|
]);
|
|
input.visualSurfaces = [
|
|
{
|
|
parentId: 2,
|
|
backendNodeId: 99,
|
|
frameId: "child",
|
|
renderingKind: "canvas",
|
|
visibleRect: { x: 10, y: 20, w: 800, h: 500 },
|
|
label: "Data grid",
|
|
memberCount: 2,
|
|
},
|
|
];
|
|
const out = renderVom(input);
|
|
|
|
expect(out.text).toContain(
|
|
'@e1 surface "Data grid" [rendering=canvas; layers=2; visual-only; requires=image-understanding; use: bsk screenshot --ref @e1]',
|
|
);
|
|
expect(out.refs).toContainEqual(
|
|
expect.objectContaining({
|
|
ref: "e1",
|
|
backendNodeId: 99,
|
|
kind: "surface",
|
|
visibleRect: { x: 10, y: 20, w: 800, h: 500 },
|
|
capabilities: ["screenshot"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("renders an unnamed visual surface with a stable generic label", () => {
|
|
const input = scene([node({ id: 1, role: "RootWebArea", frameId: "main" })]);
|
|
input.visualSurfaces = [
|
|
{
|
|
parentId: 1,
|
|
backendNodeId: 99,
|
|
frameId: "main",
|
|
renderingKind: "canvas",
|
|
visibleRect: { x: 10, y: 20, w: 800, h: 500 },
|
|
memberCount: 1,
|
|
},
|
|
];
|
|
|
|
const out = renderVom(input);
|
|
|
|
expect(out.text).toContain(
|
|
'@e1 surface "canvas visual surface" [rendering=canvas; visual-only; requires=image-understanding; use: bsk screenshot --ref @e1]',
|
|
);
|
|
expect(out.refs).toContainEqual(
|
|
expect.objectContaining({
|
|
ref: "e1",
|
|
name: "canvas visual surface",
|
|
kind: "surface",
|
|
capabilities: ["screenshot"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not derive handle context across frame scopes", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({
|
|
id: 1,
|
|
role: "RootWebArea",
|
|
frameId: "main",
|
|
contextScopeId: "main",
|
|
}),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "StaticText",
|
|
name: "创建于",
|
|
frameId: "main",
|
|
contextScopeId: "main",
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 1,
|
|
role: "Iframe",
|
|
frameId: "main",
|
|
contextScopeId: "main",
|
|
}),
|
|
node({
|
|
id: 4,
|
|
parentId: 3,
|
|
role: "button",
|
|
name: "表格视图",
|
|
frameId: "child",
|
|
contextScopeId: "child",
|
|
}),
|
|
node({
|
|
id: 5,
|
|
parentId: 1,
|
|
role: "button",
|
|
name: "表格视图",
|
|
frameId: "main",
|
|
contextScopeId: "main",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 button "表格视图"');
|
|
expect(out.text).not.toContain('@e1 button "表格视图" [ctx: 创建于]');
|
|
});
|
|
|
|
it("preserves handle context within a frame scope", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", contextScopeId: "main" }),
|
|
node({ id: 2, parentId: 1, role: "Iframe", contextScopeId: "main" }),
|
|
node({
|
|
id: 3,
|
|
parentId: 2,
|
|
role: "StaticText",
|
|
name: "工具栏",
|
|
contextScopeId: "child",
|
|
}),
|
|
node({
|
|
id: 4,
|
|
parentId: 2,
|
|
role: "button",
|
|
name: "更多",
|
|
contextScopeId: "child",
|
|
}),
|
|
node({
|
|
id: 5,
|
|
parentId: 1,
|
|
role: "button",
|
|
name: "更多",
|
|
contextScopeId: "main",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 button "更多" [ctx: 工具栏]');
|
|
});
|
|
|
|
it("always emits a complete @vom single-layer document when there is no blocker", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Shop" }),
|
|
node({ id: 2, parentId: 1, role: "navigation", name: "主菜单" }),
|
|
node({ id: 3, parentId: 2, role: "link", name: "首页", tag: "a" }),
|
|
node({ id: 4, parentId: 1, role: "main" }),
|
|
node({ id: 5, parentId: 4, role: "heading", name: "今日推荐" }),
|
|
node({ id: 6, parentId: 4, role: "button", name: "加入购物车", tag: "button" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toBe(
|
|
[
|
|
"@vom 1",
|
|
"@view 1280x720",
|
|
"@layers 1 focus=L1",
|
|
"L1 page",
|
|
' RootWebArea "Shop"',
|
|
' navigation "主菜单"',
|
|
' @e1 link "首页"',
|
|
" main",
|
|
' heading "今日推荐"',
|
|
' @e2 button "加入购物车"',
|
|
].join("\n"),
|
|
);
|
|
expect(coreRefs(out.refs)).toEqual([
|
|
{ ref: "e1", backendNodeId: 3 },
|
|
{ ref: "e2", backendNodeId: 6 },
|
|
]);
|
|
expect(out.truncated).toBe(false);
|
|
});
|
|
|
|
it("does not assign refs to structural or display-only nodes", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "heading", name: "Title" }),
|
|
node({ id: 3, parentId: 1, role: "img", name: "Logo" }),
|
|
node({ id: 4, parentId: 1, role: "button", name: "Continue", tag: "button" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain(' RootWebArea "Doc"');
|
|
expect(out.text).toContain(' heading "Title"');
|
|
expect(out.text).toContain(' img "Logo"');
|
|
expect(out.text).toContain(' @e1 button "Continue"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 4 }]);
|
|
});
|
|
|
|
it("assigns refs to listbox controls", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "listbox", name: "Choices" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain(' @e1 listbox "Choices"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 2 }]);
|
|
});
|
|
|
|
it("annotates external links regardless of role casing", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "Link",
|
|
name: "Docs",
|
|
href: "docs.example.org",
|
|
tag: "a",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 Link "Docs" [→ docs.example.org]');
|
|
});
|
|
|
|
it("renders additional structural roles without assigning refs", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "WebArea" }),
|
|
node({ id: 2, parentId: 1, role: "Iframe" }),
|
|
node({ id: 3, parentId: 1, role: "alertdialog" }),
|
|
node({ id: 4, parentId: 3, role: "section" }),
|
|
node({ id: 5, parentId: 4, role: "button", name: "Close", tag: "button" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain(" WebArea");
|
|
expect(out.text).toContain(" Iframe");
|
|
expect(out.text).toContain(" alertdialog");
|
|
expect(out.text).toContain(" section");
|
|
expect(out.text).toContain(' @e1 button "Close"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 5 }]);
|
|
});
|
|
|
|
it("skips generic nodes without adding an extra indentation level", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "generic" }),
|
|
node({ id: 3, parentId: 2, role: "button", name: "OK", tag: "button" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).not.toContain("generic");
|
|
expect(out.text).toContain(' @e1 button "OK"');
|
|
});
|
|
|
|
it("masks sensitive values and never emits the clear text value", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Login" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "textbox",
|
|
name: "密码",
|
|
value: "hunter2",
|
|
tag: "input",
|
|
sensitive: true,
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 textbox "密码" [filled] ="•••"');
|
|
expect(out.text).not.toContain("hunter2");
|
|
});
|
|
|
|
it("renders a sensitive filled state without retaining its clear text value", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Login" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "textbox",
|
|
name: "密码",
|
|
tag: "input",
|
|
sensitive: true,
|
|
inputState: "filled",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 textbox "密码" [filled] ="•••"');
|
|
});
|
|
|
|
it("does not imply an empty sensitive textbox has a value", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Login" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "textbox",
|
|
name: "密码",
|
|
tag: "input",
|
|
sensitive: true,
|
|
inputState: "empty",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 textbox "密码" [empty]');
|
|
expect(out.text).not.toContain('="•••"');
|
|
});
|
|
|
|
it("continues rendering later siblings after a branch exceeds maxDepth", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "section" }),
|
|
node({ id: 3, parentId: 2, role: "heading", name: "Too deep" }),
|
|
node({ id: 4, parentId: 1, role: "button", name: "Later", tag: "button" }),
|
|
]),
|
|
{ maxDepth: 2 },
|
|
);
|
|
|
|
expect(out.truncated).toBe(true);
|
|
expect(out.text).toContain(' RootWebArea "Doc"');
|
|
expect(out.text).toContain(" section");
|
|
expect(out.text).not.toContain("Too deep");
|
|
expect(out.text).toContain(' @e1 button "Later"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 4 }]);
|
|
});
|
|
|
|
it("truncates when maxTokens is exceeded while keeping refs in sync with rendered text", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "button", name: "First", tag: "button" }),
|
|
node({ id: 3, parentId: 1, role: "button", name: "Second", tag: "button" }),
|
|
]),
|
|
{ maxTokens: 22 },
|
|
);
|
|
|
|
expect(out.truncated).toBe(true);
|
|
const renderedRefs = new Set(Array.from(out.text.matchAll(/@(e\d+)/g), (m) => m[1]));
|
|
expect(out.refs.every((r) => renderedRefs.has(r.ref))).toBe(true);
|
|
});
|
|
|
|
it("recovers custom clickable controls from DOM-only signals", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "generic",
|
|
text: "Open settings",
|
|
cursor: "pointer",
|
|
attrs: { onclick: "handleOpen()" },
|
|
rect: { x: 20, y: 20, w: 160, h: 40 },
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 button "Open settings"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 2 }]);
|
|
});
|
|
|
|
it("does not treat implementation CSS classes as control names", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "generic",
|
|
cursor: "pointer",
|
|
attrs: { onclick: "handleClick()" },
|
|
rect: { x: 20, y: 20, w: 40, h: 40 },
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 2,
|
|
role: "img",
|
|
tag: "svg",
|
|
attrs: { class: "kocomz" },
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).not.toContain("kocomz");
|
|
expect(out.refs).toEqual([]);
|
|
});
|
|
|
|
it("recovers icon controls only from explicit semantic icon evidence", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "generic",
|
|
cursor: "pointer",
|
|
attrs: { onclick: "handleFilter()" },
|
|
rect: { x: 20, y: 20, w: 40, h: 40 },
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 2,
|
|
role: "img",
|
|
tag: "svg",
|
|
attrs: { "aria-label": "Filter rows", class: "kocomz" },
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 button "Filter rows"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 2 }]);
|
|
});
|
|
|
|
it("keeps a semantic image as evidence for its clickable wrapper", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "generic",
|
|
cursor: "pointer",
|
|
rect: { x: 20, y: 20, w: 40, h: 40 },
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 2,
|
|
role: "img",
|
|
name: "Account",
|
|
cursor: "pointer",
|
|
tag: "div",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 button "Account"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 2 }]);
|
|
});
|
|
|
|
it("keeps the deepest custom control when wrapper and child both look clickable", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "generic",
|
|
text: "Card",
|
|
cursor: "pointer",
|
|
attrs: { onclick: "handleCard()" },
|
|
rect: { x: 20, y: 20, w: 280, h: 120 },
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 2,
|
|
role: "generic",
|
|
text: "Details",
|
|
cursor: "pointer",
|
|
attrs: { onclick: "handleDetails()" },
|
|
rect: { x: 40, y: 70, w: 90, h: 32 },
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).not.toContain('@e1 button "Card"');
|
|
expect(out.text).toContain('@e1 button "Details"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 3 }]);
|
|
});
|
|
|
|
it("does not recover clickable wrappers around native controls", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "generic",
|
|
text: "Checkout",
|
|
cursor: "pointer",
|
|
attrs: { onclick: "handleCheckout()" },
|
|
hasNativeDescendant: true,
|
|
rect: { x: 20, y: 20, w: 280, h: 120 },
|
|
}),
|
|
node({ id: 3, parentId: 2, role: "button", name: "Pay", tag: "button" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).not.toContain('@e1 button "Checkout"');
|
|
expect(out.text).toContain('@e1 button "Pay"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 3 }]);
|
|
});
|
|
|
|
it("adds context to duplicate weak action labels", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Admin" }),
|
|
node({ id: 2, parentId: 1, role: "section", name: "Project Alpha" }),
|
|
node({ id: 3, parentId: 2, role: "button", name: "View", tag: "button" }),
|
|
node({ id: 4, parentId: 1, role: "section", name: "Project Beta" }),
|
|
node({ id: 5, parentId: 4, role: "button", name: "View", tag: "button" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 button "View" [ctx: Project Alpha]');
|
|
expect(out.text).toContain('@e2 button "View" [ctx: Project Beta]');
|
|
});
|
|
|
|
it("filters noisy template/layout text from weak label context", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "section",
|
|
name: "top 开始 top 结束 bigpic、登录 开始 <![endif]",
|
|
}),
|
|
node({ id: 3, parentId: 2, role: "button", name: "更多", tag: "button" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 button "更多"');
|
|
expect(out.text).not.toContain("[ctx:");
|
|
});
|
|
|
|
it("keeps active-region filtering disabled by default", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "button",
|
|
name: "Background",
|
|
tag: "button",
|
|
rect: { x: 120, y: 120, w: 120, h: 40 },
|
|
paintOrder: 1,
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 1,
|
|
role: "generic",
|
|
tag: "div",
|
|
rect: { x: 100, y: 100, w: 180, h: 120 },
|
|
paintOrder: 10,
|
|
position: "fixed",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain("@layers 1 focus=L1");
|
|
expect(out.text).toContain('@e1 button "Background"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 2 }]);
|
|
});
|
|
|
|
it("can filter refs blocked by active positioned regions without changing layer output", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "button",
|
|
name: "Background",
|
|
tag: "button",
|
|
rect: { x: 120, y: 120, w: 120, h: 40 },
|
|
paintOrder: 1,
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 1,
|
|
role: "generic",
|
|
tag: "div",
|
|
rect: { x: 100, y: 100, w: 180, h: 120 },
|
|
paintOrder: 10,
|
|
position: "fixed",
|
|
}),
|
|
node({
|
|
id: 4,
|
|
parentId: 1,
|
|
role: "button",
|
|
name: "Foreground",
|
|
tag: "button",
|
|
rect: { x: 320, y: 120, w: 120, h: 40 },
|
|
paintOrder: 11,
|
|
}),
|
|
]),
|
|
{ activeRegionPolicy: true },
|
|
);
|
|
|
|
expect(out.text).toContain("@layers 1 focus=L1");
|
|
expect(out.text).toContain("L1 page");
|
|
expect(out.text).not.toContain("active-region");
|
|
expect(out.text).not.toContain("Background");
|
|
expect(out.text).toContain('@e1 button "Foreground"');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 4 }]);
|
|
});
|
|
|
|
it("keeps paint-order comparisons inside their frame document", () => {
|
|
const out = renderVom(
|
|
{
|
|
...scene([
|
|
node({ id: 1, role: "RootWebArea", frameId: "main" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "button",
|
|
name: "Top-level action",
|
|
tag: "button",
|
|
rect: { x: 120, y: 120, w: 120, h: 40 },
|
|
paintOrder: 2,
|
|
frameId: "main",
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 1,
|
|
role: "Iframe",
|
|
tag: "iframe",
|
|
rect: { x: 0, y: 0, w: 1000, h: 700 },
|
|
paintOrder: 1,
|
|
frameId: "main",
|
|
}),
|
|
node({
|
|
id: 4,
|
|
parentId: 3,
|
|
role: "generic",
|
|
rect: { x: 0, y: 0, w: 1000, h: 700 },
|
|
paintOrder: 100,
|
|
position: "fixed",
|
|
frameId: "child",
|
|
}),
|
|
node({
|
|
id: 5,
|
|
parentId: 4,
|
|
role: "button",
|
|
name: "Child action",
|
|
tag: "button",
|
|
rect: { x: 120, y: 120, w: 120, h: 40 },
|
|
paintOrder: 101,
|
|
frameId: "child",
|
|
}),
|
|
]),
|
|
rootFrameId: "main",
|
|
},
|
|
{ activeRegionPolicy: true },
|
|
);
|
|
|
|
expect(out.text).toContain("@layers 1 focus=L1");
|
|
expect(out.text).toContain('button "Top-level action"');
|
|
expect(out.text).toContain('button "Child action"');
|
|
});
|
|
|
|
it("uses the iframe owner when a parent-frame region covers child content", () => {
|
|
const out = renderVom(
|
|
{
|
|
...scene([
|
|
node({ id: 1, role: "RootWebArea", frameId: "main" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "Iframe",
|
|
tag: "iframe",
|
|
rect: { x: 100, y: 100, w: 400, h: 300 },
|
|
paintOrder: 1,
|
|
frameId: "main",
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 2,
|
|
role: "button",
|
|
name: "Covered child action",
|
|
tag: "button",
|
|
rect: { x: 150, y: 150, w: 120, h: 40 },
|
|
paintOrder: 100,
|
|
frameId: "child",
|
|
}),
|
|
node({
|
|
id: 4,
|
|
parentId: 1,
|
|
role: "generic",
|
|
rect: { x: 120, y: 120, w: 200, h: 120 },
|
|
paintOrder: 2,
|
|
position: "fixed",
|
|
frameId: "main",
|
|
}),
|
|
]),
|
|
rootFrameId: "main",
|
|
},
|
|
{ activeRegionPolicy: true },
|
|
);
|
|
|
|
expect(out.text).not.toContain("Covered child action");
|
|
});
|
|
|
|
it("renders conditional surface items inline on the trigger", () => {
|
|
const out = renderVom({
|
|
...scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "button", name: "Products", tag: "button" }),
|
|
]),
|
|
surfaces: [
|
|
{
|
|
triggerId: 2,
|
|
triggerAction: "hover",
|
|
subItems: ["Shoes", "Bags", "Accessories"],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(out.text).toContain('@e1 button "Products" [hover first: Shoes | Bags | Accessories]');
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 2 }]);
|
|
});
|
|
|
|
it("injects active scope blocks immediately after their trigger without refs", () => {
|
|
const out = renderVom({
|
|
...scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "tab", name: "Reviews (12)", tag: "button" }),
|
|
]),
|
|
activeScopeBlocks: [
|
|
{
|
|
triggerId: 2,
|
|
label: "Reviews (12)",
|
|
lines: ["Jane - ear cups are small", "Bob - great sound"],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(out.text).toContain(
|
|
[
|
|
' @e1 tab "Reviews (12)"',
|
|
" [§ active: Reviews (12)]",
|
|
" Jane - ear cups are small",
|
|
" Bob - great sound",
|
|
].join("\n"),
|
|
);
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 2 }]);
|
|
});
|
|
|
|
it("does not render redundant children inside named ref controls", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "button", name: "Save", tag: "button" }),
|
|
node({ id: 3, parentId: 2, role: "img", name: "disk icon", tag: "svg" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 button "Save"');
|
|
expect(out.text).not.toContain("disk icon");
|
|
expect(coreRefs(out.refs)).toEqual([{ ref: "e1", backendNodeId: 2 }]);
|
|
});
|
|
|
|
it("does not render redundant children inside native text inputs", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "textbox",
|
|
name: "手机号",
|
|
value: "13311030827",
|
|
tag: "input",
|
|
}),
|
|
node({ id: 3, parentId: 2, role: "StaticText", name: "13311030827" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 textbox "手机号" [filled] ="13311030827"');
|
|
expect(out.text).not.toContain('StaticText "13311030827"');
|
|
});
|
|
|
|
it("renders placeholder separately from input value and state", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
role: "textbox",
|
|
name: "手机号",
|
|
value: "没有手机号的用编码代替",
|
|
placeholder: "手机号",
|
|
inputState: "default",
|
|
tag: "input",
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain(
|
|
'@e1 textbox "手机号" [default] placeholder="手机号" ="没有手机号的用编码代替"',
|
|
);
|
|
});
|
|
|
|
it("keeps only local context for duplicate labels", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "北京市小客车指标调控管理信息系统" }),
|
|
node({ id: 2, parentId: 1, role: "section" }),
|
|
node({ id: 3, parentId: 2, role: "StaticText", name: "账号" }),
|
|
node({ id: 4, parentId: 2, role: "link", name: "操作说明", tag: "a" }),
|
|
node({ id: 5, parentId: 1, role: "section" }),
|
|
node({ id: 6, parentId: 5, role: "StaticText", name: "配置指标" }),
|
|
node({ id: 7, parentId: 5, role: "link", name: "操作说明", tag: "a" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 link "操作说明" [ctx: 账号]');
|
|
expect(out.text).toContain('@e2 link "操作说明" [ctx: 配置指标]');
|
|
expect(out.text).not.toContain("北京市小客车指标调控管理信息系统]");
|
|
});
|
|
|
|
it("does not add low-value root context to duplicated brand links", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "北京市小客车指标调控管理信息系统" }),
|
|
node({ id: 2, parentId: 1, role: "link", name: "北京市交通委员会", tag: "a" }),
|
|
node({ id: 3, parentId: 1, role: "link", name: "北京市交通委员会", tag: "a" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('@e1 link "北京市交通委员会"');
|
|
expect(out.text).toContain('@e2 link "北京市交通委员会"');
|
|
expect(out.text).not.toContain("[ctx:");
|
|
});
|
|
|
|
it("does not render redundant table cell descendants when the cell name covers them", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "table" }),
|
|
node({ id: 3, parentId: 2, role: "row" }),
|
|
node({ id: 4, parentId: 3, role: "cell", name: "申请人姓名:刘珺瑶" }),
|
|
node({ id: 5, parentId: 4, role: "StaticText", name: "申请人姓名:" }),
|
|
node({ id: 6, parentId: 4, role: "StaticText", name: "刘珺瑶" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('cell "申请人姓名:刘珺瑶"');
|
|
expect(out.text).not.toContain('StaticText "申请人姓名:"');
|
|
expect(out.text).not.toContain('StaticText "刘珺瑶"');
|
|
});
|
|
|
|
it("keeps interactive descendants inside table cells", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Doc" }),
|
|
node({ id: 2, parentId: 1, role: "table" }),
|
|
node({ id: 3, parentId: 2, role: "row" }),
|
|
node({ id: 4, parentId: 3, role: "cell", name: "操作:下载" }),
|
|
node({ id: 5, parentId: 4, role: "link", name: "下载", tag: "a" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain('cell "操作:下载"');
|
|
expect(out.text).toContain('@e1 link "下载"');
|
|
});
|
|
});
|
|
|
|
describe("renderVom double-layer page", () => {
|
|
it("renders a blocking modal first and folds the base page into an occluded summary", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({
|
|
id: 1,
|
|
role: "RootWebArea",
|
|
name: "Shop",
|
|
tag: "body",
|
|
rect: { x: 0, y: 0, w: 1000, h: 3000 },
|
|
}),
|
|
node({ id: 10, parentId: 1, role: "button", name: "底层按钮", tag: "button" }),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
tag: "div",
|
|
rect: { x: 0, y: 0, w: 1280, h: 720 },
|
|
paintOrder: 50,
|
|
position: "fixed",
|
|
pointerEvents: "auto",
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 1,
|
|
role: "dialog",
|
|
name: "提示",
|
|
modal: true,
|
|
rect: { x: 400, y: 200, w: 480, h: 320 },
|
|
paintOrder: 60,
|
|
position: "fixed",
|
|
}),
|
|
node({
|
|
id: 4,
|
|
parentId: 3,
|
|
role: "textbox",
|
|
name: "密码",
|
|
value: "secret",
|
|
tag: "input",
|
|
sensitive: true,
|
|
}),
|
|
node({ id: 5, parentId: 3, role: "button", name: "确认", tag: "button" }),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toBe(
|
|
[
|
|
"@vom 1",
|
|
"@view 1280x720",
|
|
"@layers 2 focus=L1",
|
|
"L1 modal cover=100%",
|
|
' dialog "提示"',
|
|
' @e1 textbox "密码" [filled] ="•••"',
|
|
' @e2 button "确认"',
|
|
"L2 page … occluded by L1 (~2 nodes, not actionable)",
|
|
].join("\n"),
|
|
);
|
|
expect(out.text).not.toContain("底层按钮");
|
|
expect(coreRefs(out.refs)).toEqual([
|
|
{ ref: "e1", backendNodeId: 4 },
|
|
{ ref: "e2", backendNodeId: 5 },
|
|
]);
|
|
});
|
|
|
|
it("renders iframe descendants inside the top layer through parentId without iframe-specific renderer code", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({
|
|
id: 1,
|
|
role: "RootWebArea",
|
|
name: "JD",
|
|
tag: "body",
|
|
rect: { x: 0, y: 0, w: 1000, h: 3000 },
|
|
}),
|
|
node({
|
|
id: 2,
|
|
parentId: 1,
|
|
tag: "div",
|
|
rect: { x: 0, y: 0, w: 1280, h: 720 },
|
|
paintOrder: 50,
|
|
position: "fixed",
|
|
}),
|
|
node({
|
|
id: 3,
|
|
parentId: 2,
|
|
role: "Iframe",
|
|
name: "登录框",
|
|
tag: "iframe",
|
|
rect: { x: 100, y: 240, w: 800, h: 400 },
|
|
paintOrder: 51,
|
|
}),
|
|
node({ id: 101, parentId: 3, role: "textbox", name: "请输入手机号", tag: "input" }),
|
|
node({
|
|
id: 102,
|
|
parentId: 3,
|
|
role: "textbox",
|
|
name: "密码",
|
|
value: "secret",
|
|
tag: "input",
|
|
sensitive: true,
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(out.text).toContain("L1 modal cover=100%");
|
|
expect(out.text).toContain(' Iframe "登录框"');
|
|
expect(out.text).toContain(' @e1 textbox "请输入手机号" [empty]');
|
|
expect(out.text).toContain(' @e2 textbox "密码" [filled] ="•••"');
|
|
expect(out.refs.map((r) => r.backendNodeId)).toEqual([101, 102]);
|
|
});
|
|
|
|
it("redacts form values and returns semantic metadata for rendered refs", () => {
|
|
const out = renderVom(
|
|
scene([
|
|
node({ id: 1, role: "RootWebArea", name: "Form" }),
|
|
node({
|
|
id: 2,
|
|
backendNodeId: 42,
|
|
frameId: "child-frame",
|
|
parentId: 1,
|
|
role: "textbox",
|
|
name: "Email",
|
|
value: "user@example.com",
|
|
inputState: "filled",
|
|
tag: "input",
|
|
}),
|
|
]),
|
|
{ redactValues: true },
|
|
);
|
|
|
|
expect(out.text).toContain('[filled] ="•••"');
|
|
expect(out.text).not.toContain("user@example.com");
|
|
expect(out.refs[0]).toMatchObject({
|
|
ref: "e1",
|
|
backendNodeId: 42,
|
|
frameId: "child-frame",
|
|
role: "textbox",
|
|
name: "Email",
|
|
line: expect.any(Number),
|
|
});
|
|
expect(out.text.split("\n")[out.refs[0]?.line ?? -1]).toContain('@e1 textbox "Email"');
|
|
});
|
|
});
|