import assert from "node:assert/strict"; import { test } from "node:test"; 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 { 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, }; } async function withPage(html: string, callback: (page: Page) => Promise): Promise { 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"); assert.equal(candidates[0].framework, "playwright"); assert.equal(candidates.some((candidate) => candidate.strategy === "testId"), true); assert.equal(candidates.at(-1)?.strategy, "css"); }); test("locator generation keeps CSS as fallback for id and name", () => { const candidates = generateLocators( "el_input", raw({ tag: "input", role: "textbox", accessibleName: "Query", text: null, label: "Query", placeholder: "Search customers", attributes: { id: "queryInput", name: "q" }, }), ); 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(``, 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(``, 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(``, 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); } }); });