feat: verify locators with context evidence

This commit is contained in:
赵义仑
2026-06-16 20:02:25 +08:00
parent 581724ea42
commit c41f260e51
5 changed files with 281 additions and 9 deletions

View File

@@ -40,6 +40,16 @@ test("scan captures a state-local inventory without clicking high-risk actions",
assert.equal(graph.elements.some((element) => element.accessibleName === "Delete account"), false);
});
test("duplicated row actions are verified with semantic locator evidence", async () => {
const graph = await withTempOutput("duplicate-locators", (out) => scanUrl({ url: fixturePath, out }));
const editButtons = graph.elements.filter((element) => element.accessibleName === "Edit");
assert.equal(editButtons.length, 2);
for (const edit of editButtons) {
assert.equal(edit.locators.some((locator) => locator.verified && locator.identity?.sameAccessibleName), true);
assert.equal(edit.locators.some((locator) => locator.scopes.some((scope) => scope.kind === "row")), true);
}
});
test("graph metadata records generation context and policy versions", async () => {
const graph = await withTempOutput("metadata", (out) => scanUrl({ url: fixturePath, out }));
assert.equal(graph.schemaVersion, "0.2");

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { generateLocators } from "../src/locator.js";
import { chromium, type Page } from "playwright";
import { generateLocators, verifyLocators } from "../src/locator.js";
import type { RawActionableElement } from "../src/types.js";
function raw(overrides: Partial<RawActionableElement>): RawActionableElement {
@@ -31,6 +32,17 @@ function raw(overrides: Partial<RawActionableElement>): RawActionableElement {
};
}
async function withPage<T>(html: string, callback: (page: Page) => Promise<T>): Promise<T> {
const browser = await chromium.launch();
const page = await browser.newPage();
try {
await page.setContent(html);
return await callback(page);
} finally {
await browser.close();
}
}
test("locator generation prioritizes user-facing Playwright locators before CSS fallbacks", () => {
const candidates = generateLocators("el_test", raw({}));
assert.equal(candidates[0].strategy, "role");
@@ -55,3 +67,100 @@ test("locator generation keeps CSS as fallback for id and name", () => {
assert.equal(candidates.some((candidate) => candidate.expression.includes("#queryInput")), true);
assert.equal(candidates.some((candidate) => candidate.expression.includes("[name=")), true);
});
test("locator candidates carry context-scope evidence for duplicated actions", () => {
const candidates = generateLocators(
"el_edit",
raw({
uid: "button_4_Edit",
tag: "button",
role: "button",
accessibleName: "Edit",
text: "Edit",
attributes: {},
context: {
dialog: null,
form: null,
section: { kind: "section", name: "Customers", text: "Customers Acme Co Active Edit" },
row: { kind: "row", name: null, text: "Acme Co Active Edit" },
landmark: null,
listitem: null,
},
}),
);
assert.equal(candidates[0].scopes.some((scope) => scope.kind === "row"), true);
});
test("placeholder-only input locator verifies identity from raw placeholder evidence", async () => {
await withPage(`<input placeholder="Search customers" />`, async (page) => {
const rawElement = raw({
uid: "input_0_Search customers",
tag: "input",
role: "textbox",
accessibleName: "Search customers",
identityText: "Search customers",
text: null,
label: null,
placeholder: "Search customers",
attributes: {},
visibility: { visible: true, enabled: true, editable: true, receivesEvents: true },
});
const placeholder = generateLocators("el_placeholder", rawElement).find((candidate) => candidate.strategy === "placeholder");
assert.ok(placeholder);
const [verified] = await verifyLocators(page, rawElement, [placeholder]);
assert.equal(verified.verified, true);
assert.equal(verified.matchCount, 1);
assert.equal(verified.identity?.sameAccessibleName, true);
});
});
test("label-derived input locator verifies identity from raw label evidence", async () => {
await withPage(`<label for="query">Query</label><input id="query" />`, async (page) => {
const rawElement = raw({
uid: "input_0_Query",
tag: "input",
role: "textbox",
accessibleName: "Query",
identityText: "Query",
text: null,
label: "Query",
placeholder: null,
attributes: { id: "query" },
visibility: { visible: true, enabled: true, editable: true, receivesEvents: true },
});
const label = generateLocators("el_label", rawElement).find((candidate) => candidate.strategy === "label");
assert.ok(label);
const [verified] = await verifyLocators(page, rawElement, [label]);
assert.equal(verified.verified, true);
assert.equal(verified.matchCount, 1);
assert.equal(verified.identity?.sameAccessibleName, true);
});
});
test("duplicate Edit locators are unverified and omit non-unique identity evidence", async () => {
await withPage(`<button>Edit</button><button>Edit</button>`, async (page) => {
const rawElement = raw({
uid: "button_0_Edit",
tag: "button",
role: "button",
accessibleName: "Edit",
identityText: "Edit",
text: "Edit",
attributes: {},
});
const duplicateLocators = generateLocators("el_edit", rawElement).filter((candidate) => ["role", "text"].includes(candidate.strategy));
assert.equal(duplicateLocators.length, 2);
const verified = await verifyLocators(page, rawElement, duplicateLocators);
for (const locator of verified) {
assert.equal(locator.verified, false);
assert.equal(locator.matchCount, 2);
assert.equal(locator.identity, null);
}
});
});