feat(vom): vom fix

This commit is contained in:
Ljy-0827
2026-07-27 14:53:10 +08:00
parent 4fb2e331a6
commit c002c394e6
6 changed files with 154 additions and 24 deletions
+21
View File
@@ -115,6 +115,27 @@ describe("detectBlockingLayer", () => {
expect(layer?.kind).toBe("modal");
});
it("detects modal features regardless of role or tag casing", () => {
const layer = detectBlockingLayer(
[
node({ id: 1, tag: "body", rect: { x: 0, y: 0, w: 1000, h: 800 } }),
node({
id: 2,
parentId: 1,
tag: "DIV",
role: "Dialog",
rect: { x: 300, y: 200, w: 400, h: 300 },
paintOrder: 90,
position: "fixed",
}),
],
VP,
);
expect(layer?.rootId).toBe(2);
expect(layer?.kind).toBe("modal");
});
it("ignores small toasts and pointer-events:none covers", () => {
expect(
detectBlockingLayer(
+18
View File
@@ -87,6 +87,24 @@ describe("renderVom single-layer page", () => {
expect(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([
+13 -8
View File
@@ -8,6 +8,14 @@ export const MASK_COVERAGE_THRESHOLD = 0.9;
const POSITIONED = new Set(["fixed", "absolute", "sticky"]);
const FORM_TAGS = new Set(["input", "textarea", "select"]);
function normalizedRole(node: VomNode): string {
return node.role?.toLowerCase() ?? "";
}
function normalizedTag(node: VomNode): string {
return node.tag.toLowerCase();
}
export function coverage(rect: Rect | null, vp: Viewport): number {
if (!rect || vp.width <= 0 || vp.height <= 0) return 0;
const ix = Math.max(0, Math.min(rect.x + rect.w, vp.width) - Math.max(rect.x, 0));
@@ -23,12 +31,9 @@ function isBlockingCandidate(node: VomNode): boolean {
}
function hasModalFeature(node: VomNode): boolean {
return (
node.modal === true ||
node.tag === "dialog" ||
node.role === "dialog" ||
node.role === "alertdialog"
);
const role = normalizedRole(node);
const tag = normalizedTag(node);
return node.modal === true || tag === "dialog" || role === "dialog" || role === "alertdialog";
}
function classifyLayer(
@@ -39,8 +44,8 @@ function classifyLayer(
for (const node of nodes) {
if (!members.has(node.id)) continue;
if (hasModalFeature(node)) return "modal";
if (FORM_TAGS.has(node.tag)) return "modal";
if (node.tag === "iframe") return "modal";
if (FORM_TAGS.has(normalizedTag(node))) return "modal";
if (normalizedTag(node) === "iframe") return "modal";
}
return blockerCoverage >= MASK_COVERAGE_THRESHOLD ? "mask" : "modal";
}
+8 -5
View File
@@ -166,6 +166,10 @@ function normalizedRole(node: VomNode): string {
return node.role?.toLowerCase() ?? "";
}
function normalizedTag(node: VomNode): string {
return node.tag.toLowerCase();
}
function normalizeContextKey(s: string): string {
return s.split(/\s+/).join(" ").toLowerCase();
}
@@ -296,7 +300,7 @@ function rectArea(node: VomNode): number {
function passesRecoveryGuards(node: VomNode, children: Map<number | null, VomNode[]>): boolean {
const area = rectArea(node);
const tag = node.tag.toLowerCase();
const tag = normalizedTag(node);
if (node.pointerEvents === "none") return false;
if (node.disabled || node.inert || node.insideNative) return false;
@@ -611,7 +615,7 @@ function renderNodeLine(
// For link nodes with an external href, annotate so the agent can
// distinguish external navigation from same-origin links.
if (node.role === "link" && node.href) {
if (normalizedRole(node) === "link" && node.href) {
line += ` [→ ${node.href}]`;
}
@@ -838,9 +842,8 @@ function interactionPoints(rect: Rect): Array<[number, number]> {
function isModalLike(node: VomNode): boolean {
const role = normalizedRole(node);
return (
node.modal === true || node.tag === "dialog" || role === "dialog" || role === "alertdialog"
);
const tag = normalizedTag(node);
return node.modal === true || tag === "dialog" || role === "dialog" || role === "alertdialog";
}
function activeRegionCandidatePriority(node: VomNode, viewportCoverage: number): number {