feat: capture state and element evidence
This commit is contained in:
36
tests/fixtures/context-risk.html
vendored
36
tests/fixtures/context-risk.html
vendored
@@ -58,6 +58,42 @@
|
||||
<button type="submit">Submit search</button>
|
||||
</form>
|
||||
|
||||
<form>
|
||||
<p>
|
||||
This unnamed form deliberately contains a long body of descendant text that should remain available as bounded
|
||||
context evidence but must not be promoted into the anchor name used for element identity. It describes internal
|
||||
notes, operator workflow details, historical context, and extra explanatory copy that would make a noisy and
|
||||
unstable identifier if copied into the context anchor name.
|
||||
</p>
|
||||
<label>
|
||||
Unbounded Probe
|
||||
<input name="unboundedProbe" />
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<main aria-label="Operations">
|
||||
<button data-testid="main-action">Main action</button>
|
||||
</main>
|
||||
|
||||
<section aria-label="Duplicate controls">
|
||||
<button>Duplicate</button>
|
||||
<button>Duplicate</button>
|
||||
</section>
|
||||
|
||||
<section role="dialog" aria-modal="true" class="open">
|
||||
<span>Short volatile copy</span>
|
||||
<button data-testid="short-dialog-action">Short dialog action</button>
|
||||
</section>
|
||||
|
||||
<section role="dialog" aria-modal="true" class="open">
|
||||
<p>
|
||||
This unnamed dialog carries noisy descendant copy about multi-step operator notes, escalation background,
|
||||
support annotations, internal timestamps, and other volatile details that must stay out of the dialog anchor
|
||||
name while remaining available as bounded context text evidence for nearby actionable controls.
|
||||
</p>
|
||||
<button data-testid="dialog-probe">Dialog probe</button>
|
||||
</section>
|
||||
|
||||
<button disabled data-testid="disabled-open">Open disabled panel</button>
|
||||
<button data-testid="open-settings" onclick="document.getElementById('settings-dialog').classList.add('open')">Open settings</button>
|
||||
<section id="settings-dialog" role="dialog" aria-modal="true" aria-label="Settings dialog">
|
||||
|
||||
@@ -3,9 +3,12 @@ import { test } from "node:test";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { exploreUrl, scanUrl } from "../src/engine.js";
|
||||
import { shortHash } from "../src/hash.js";
|
||||
|
||||
const fixturePath = path.resolve("tests/fixtures/context-risk.html");
|
||||
const fixtureFileUrl = pathToFileURL(fixturePath).href;
|
||||
|
||||
async function withTempOutput<T>(name: string, callback: (out: string) => Promise<T>): Promise<T> {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "action-topology-engine-test-"));
|
||||
@@ -51,6 +54,131 @@ test("graph metadata records generation context and policy versions", async () =
|
||||
assert.equal(typeof graph.metadata.inputFingerprint, "string");
|
||||
});
|
||||
|
||||
test("scan records state lifecycle, language, context anchors, and parameter surfaces", async () => {
|
||||
const graph = await withTempOutput("evidence", (out) =>
|
||||
scanUrl({ url: `${fixtureFileUrl}?utm_source=codex&gclid=ad-click&keep=1#customers`, out }),
|
||||
);
|
||||
const state = graph.states[0];
|
||||
assert.equal(state.lifecycle, "fingerprinted");
|
||||
assert.equal(state.language, "en");
|
||||
assert.equal(state.viewport.width, 1440);
|
||||
assert.equal(state.urlCanonicalKey, `${fixtureFileUrl}?keep=1`);
|
||||
assert.equal(state.evidence.some((item) => item.level === "observed" && item.source === "browser.location"), true);
|
||||
assert.equal(state.evidence.some((item) => item.level === "derived" && item.source === "snapshot.hash"), true);
|
||||
|
||||
const query = graph.elements.find((element) => element.accessibleName === "Query");
|
||||
assert.ok(query);
|
||||
assert.equal(query.parameterSurface?.name, "q");
|
||||
assert.equal(query.parameterSurface?.required, true);
|
||||
assert.equal(query.parameterSurface?.maxLength, 40);
|
||||
assert.equal(query.context.form?.name, "Search customers");
|
||||
assert.equal(query.evidence.some((item) => item.level === "observed"), true);
|
||||
|
||||
const dialogProbe = graph.elements.find((element) => element.accessibleName === "Dialog probe");
|
||||
assert.ok(dialogProbe);
|
||||
assert.equal(dialogProbe.context.dialog?.name, null);
|
||||
assert.equal(dialogProbe.context.dialog?.text?.includes("noisy descendant copy"), true);
|
||||
|
||||
const shortUnnamedDialog = graph.elements.find(
|
||||
(element) => element.tag === "section" && element.role === "dialog" && element.text?.includes("Short volatile copy"),
|
||||
);
|
||||
assert.ok(shortUnnamedDialog);
|
||||
assert.equal(shortUnnamedDialog.context.dialog?.name, null);
|
||||
assert.equal(shortUnnamedDialog.context.dialog?.text?.includes("Short volatile copy"), true);
|
||||
const shortUnnamedDialogIndex = graph.elements.findIndex((element) => element === shortUnnamedDialog);
|
||||
assert.notEqual(shortUnnamedDialogIndex, -1);
|
||||
assert.equal(
|
||||
shortUnnamedDialog.elementId,
|
||||
`el_${shortHash([
|
||||
state.stateId,
|
||||
shortUnnamedDialogIndex,
|
||||
shortUnnamedDialog.tag,
|
||||
shortUnnamedDialog.role,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
shortUnnamedDialog.context.dialog?.name,
|
||||
shortUnnamedDialog.context.form?.name,
|
||||
shortUnnamedDialog.context.section?.name,
|
||||
shortUnnamedDialog.context.landmark?.name,
|
||||
shortUnnamedDialog.context.listitem?.name,
|
||||
null,
|
||||
])}`,
|
||||
);
|
||||
|
||||
const unnamedDialog = graph.elements.find(
|
||||
(element) => element.tag === "section" && element.role === "dialog" && element.text?.includes("noisy descendant copy"),
|
||||
);
|
||||
assert.ok(unnamedDialog);
|
||||
const unnamedDialogIndex = graph.elements.findIndex((element) => element === unnamedDialog);
|
||||
assert.notEqual(unnamedDialogIndex, -1);
|
||||
assert.equal(
|
||||
unnamedDialog.elementId,
|
||||
`el_${shortHash([
|
||||
state.stateId,
|
||||
unnamedDialogIndex,
|
||||
unnamedDialog.tag,
|
||||
unnamedDialog.role,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
unnamedDialog.context.dialog?.name,
|
||||
unnamedDialog.context.form?.name,
|
||||
unnamedDialog.context.section?.name,
|
||||
unnamedDialog.context.landmark?.name,
|
||||
unnamedDialog.context.listitem?.name,
|
||||
null,
|
||||
])}`,
|
||||
);
|
||||
|
||||
const duplicates = graph.elements.filter((element) => element.accessibleName === "Duplicate");
|
||||
assert.equal(duplicates.length, 2);
|
||||
assert.notEqual(duplicates[0]?.elementId, duplicates[1]?.elementId);
|
||||
|
||||
const unboundedProbe = graph.elements.find((element) => element.accessibleName === "Unbounded Probe");
|
||||
assert.ok(unboundedProbe);
|
||||
assert.equal(unboundedProbe.context.form?.name, null);
|
||||
assert.equal(unboundedProbe.context.form?.text?.includes("long body of descendant text"), true);
|
||||
|
||||
const mainAction = graph.elements.find((element) => element.accessibleName === "Main action");
|
||||
assert.ok(mainAction);
|
||||
assert.equal(mainAction.context.section, null);
|
||||
assert.equal(mainAction.context.landmark?.name, "Operations");
|
||||
|
||||
const edit = graph.elements.find((element) => element.accessibleName === "Edit");
|
||||
assert.ok(edit);
|
||||
assert.equal(edit.context.row?.text.includes("Acme Co") || edit.context.row?.text.includes("Beta LLC"), true);
|
||||
const edits = graph.elements.filter((element) => element.accessibleName === "Edit");
|
||||
assert.equal(edits.length, 2);
|
||||
assert.notEqual(edits[0]?.elementId, edits[1]?.elementId);
|
||||
assert.notEqual(edits[0]?.context.row?.text, edits[1]?.context.row?.text);
|
||||
for (const item of edits) {
|
||||
const index = graph.elements.findIndex((element) => element === item);
|
||||
assert.notEqual(index, -1);
|
||||
assert.equal(
|
||||
item.elementId,
|
||||
`el_${shortHash([
|
||||
state.stateId,
|
||||
index,
|
||||
item.tag,
|
||||
item.role,
|
||||
item.attributes["data-testid"] || item.attributes["data-test-id"] || item.attributes["data-test"] || null,
|
||||
item.attributes.id || null,
|
||||
item.attributes.name || null,
|
||||
item.accessibleName,
|
||||
item.context.dialog?.name,
|
||||
item.context.form?.name,
|
||||
item.context.section?.name,
|
||||
item.context.landmark?.name,
|
||||
item.context.listitem?.name,
|
||||
null,
|
||||
])}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("explore records low-risk edges and keeps high-risk submit actions out of default execution", async () => {
|
||||
const graph = await withTempOutput("explore", (out) => exploreUrl({ url: fixturePath, maxActions: 8, out }));
|
||||
assert.equal(graph.edges.some((edge) => edge.riskLevel === "low" && edge.toStateId), true);
|
||||
|
||||
@@ -6,15 +6,27 @@ import type { RawActionableElement } from "../src/types.js";
|
||||
function raw(overrides: Partial<RawActionableElement>): RawActionableElement {
|
||||
return {
|
||||
uid: "button_0_Open settings",
|
||||
candidateIndex: 0,
|
||||
tag: "button",
|
||||
role: "button",
|
||||
accessibleName: "Open settings",
|
||||
identityText: "Open settings",
|
||||
text: "Open settings",
|
||||
label: null,
|
||||
placeholder: null,
|
||||
attributes: { "data-testid": "open-settings", id: "openSettings" },
|
||||
bbox: { x: 0, y: 0, width: 100, height: 30 },
|
||||
visibility: { visible: true, enabled: true, editable: false, receivesEvents: true },
|
||||
context: {
|
||||
dialog: null,
|
||||
form: null,
|
||||
section: null,
|
||||
row: null,
|
||||
landmark: null,
|
||||
listitem: null,
|
||||
},
|
||||
parameterSurface: null,
|
||||
evidence: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,16 @@ function element(overrides: Partial<ActionableElement>): ActionableElement {
|
||||
shadowPath: [],
|
||||
interactability: { visible: true, enabled: true, editable: false, receivesEvents: true },
|
||||
locators: [],
|
||||
context: {
|
||||
dialog: null,
|
||||
form: null,
|
||||
section: null,
|
||||
row: null,
|
||||
landmark: null,
|
||||
listitem: null,
|
||||
},
|
||||
parameterSurface: null,
|
||||
evidence: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user