import { describe, expect, it } from "vitest"; import { runScript, type EngineDeps } from "../../runner/engine"; import { LockStore, lockKey } from "../../runner/lockfile"; import { parseScript } from "../../runner/parser"; import type { ElementDescriptor } from "../../shared/descriptor"; const script = (text: string) => { const r = parseScript(text); if (!r.ok) throw new Error(JSON.stringify(r.errors)); return r.script; }; const full = (text: string): ElementDescriptor => ({ tag: "button", role: "button", name: text, text, attrs: { "aria-label": null, "data-testid": null, id: null, name: null, type: null, placeholder: null }, classes: [], css: "button", xpath: `/html/body/button[${text.length}]`, near: { heading: null, previousText: null, nextText: null }, rect: { x: 1, y: 2, w: 3, h: 4 }, }); /** A bridge that answers resolve with the descriptor chosen by the target's text (or `text:` of drag's second target). */ function bridge(over: Record) => Record> = {}) { let t = 0; const deps: EngineDeps = { request: async (body) => { t += 10; if (over[body.cmd]) return { id: 1, ...over[body.cmd]!(body) } as never; if (body.cmd === "resolve") return { id: 1, ok: true, descriptor: full(String((body.target as Record).text ?? "x")) } as never; return { id: 1, ok: true } as never; }, now: () => t, sleep: async () => undefined, }; return deps; } const clock = () => "2026-10-11T00:00:00Z"; describe("the engine and the lock file (P3-1)", () => { it("captures a descriptor on the first successful resolve, under the step's name and the target as written", async () => { const lock = new LockStore(undefined, clock); const r = await runScript(script(`test "t"\n step "send it"\n click text:"Send"\n step "check"\n verify text:"Sent"\n`), { ...bridge(), lock }); expect(r.ok).toBe(true); expect(lock.find("send it", 'text:"Send"')?.text).toBe("Send"); expect(Object.keys(lock.toFile().targets)).toEqual([lockKey("send it", 'text:"Send"')]); expect(r.lock).toEqual({ captured: 1, reused: 0, ignored: 0, repaired: 0 }); }); it("reuses an existing entry and leaves it exactly as it was", async () => { const first = new LockStore(undefined, clock); const s = script(`test "t"\n step "send it"\n click text:"Send"\n`); await runScript(s, { ...bridge(), lock: first }); const second = new LockStore(first.toFile(), () => "2031-01-01T00:00:00Z"); const r = await runScript(s, { ...bridge(), lock: second }); expect(r.lock).toEqual({ captured: 0, reused: 1, ignored: 0, repaired: 0 }); expect(second.dirty).toBe(false); expect(second.toFile()).toEqual(first.toFile()); }); it("a target with the same words in a different step gets its own entry", async () => { const lock = new LockStore(undefined, clock); await runScript(script(`test "t"\n step "one"\n click text:"Go"\n step "two"\n click text:"Go"\n`), { ...bridge(), lock }); expect(Object.keys(lock.toFile().targets)).toHaveLength(2); }); it("records a ${var} target as written, not as filled", async () => { const lock = new LockStore(undefined, clock); await runScript(script(`test "t"\n step "pick"\n set who "Ann"\n click text:"\${who}"\n`), { ...bridge(), lock }); expect(lock.find("pick", 'text:"${who}"')).toBeDefined(); }); it("captures both targets of a drag", async () => { const lock = new LockStore(undefined, clock); await runScript(script(`test "t"\n step "move"\n drag text:"A" to:text:"B"\n`), { ...bridge(), lock }); expect(lock.find("move", 'text:"A"')).toBeDefined(); expect(lock.find("move", 'text:"B"')).toBeDefined(); }); it("captures nothing when the target is not found, and the run still fails as before", async () => { const lock = new LockStore(undefined, clock); const r = await runScript(script(`test "t"\n step "s"\n click text:"Nope"\n`), { ...bridge({ resolve: () => ({ ok: false, reason: "not-found", detail: {} }) }), lock }); expect(r.ok).toBe(false); expect(lock.dirty).toBe(false); expect(r.lock).toEqual({ captured: 0, reused: 0, ignored: 0, repaired: 0 }); }); it("skips a descriptor that is not the full shape (an older extension) and still runs", async () => { const lock = new LockStore(undefined, clock); const r = await runScript(script(`test "t"\n step "s"\n click text:"Go"\n`), { ...bridge({ resolve: () => ({ ok: true, descriptor: { tag: "button", role: "button", name: "Go", text: "Go", xpath: "/x" } }) }), lock }); expect(r.ok).toBe(true); expect(lock.dirty).toBe(false); }); it("without a lock the result carries no lock summary and nothing changes", async () => { const r = await runScript(script(`test "t"\n step "s"\n click text:"Go"\n`), bridge()); expect(r.ok).toBe(true); expect(r.lock).toBeUndefined(); }); });