Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153import { describe, expect, it } from "vitest";import { formatReport, runScript, type EngineDeps } from "../../runner/engine";import { LockStore } from "../../runner/lockfile";import { parseScript } from "../../runner/parser";import { desc } from "../helpers/descriptors";
const script = (text: string) => { const r = parseScript(text); if (!r.ok) throw new Error(JSON.stringify(r.errors)); return r.script;};type Body = { cmd: string; [k: string]: unknown };interface Sent { body: Body }
/** Each profile has a Chrome of its own: tabs are numbered per profile (A opens 7, B opens 21). */function bridge(answerFor: (b: Body, sent: Sent[]) => Record<string, unknown> | undefined = () => undefined) { const sent: Sent[] = []; const bodies: Body[] = []; let t = 0; const deps: EngineDeps = { request: async (body) => { sent.push({ body }); bodies.push(body); t += 10; const custom = answerFor(body, sent); if (custom) return { id: sent.length, ...custom } as never; if (body.cmd === "navigate") return { id: sent.length, ok: true, tab: body.profile === "test-buster" ? 21 : 7 } as never; if (body.cmd === "resolve") return { id: sent.length, ok: true, descriptor: desc({ xpath: "/html/body/button", text: "x" }), matches: 1, repaired: false } as never; if (body.cmd === "screenshot") return { id: sent.length, ok: true, png: "AAAA" } as never; return { id: sent.length, ok: true } as never; }, now: () => t, sleep: async () => undefined, }; return { deps, sent: bodies, all: sent };}const GIFT = `test "gift" needs session test-qian needs session test-buster start http://127.0.0.1:4780/gift.html step "qian makes a gift" click text:"Create gift" step "buster claims it" goto http://127.0.0.1:4780/claim.html on:test-buster click text:"Claim" on:test-buster step "qian sees it claimed" verify text:"Claimed" on:test-qian`;
describe("commands on several profiles (P5-6)", () => { it("P5-6.a: a command with on: is sent to that profile and the others to the default, and every request says which", async () => { const b = bridge(); const r = await runScript(script(GIFT), b.deps); expect(r.ok).toBe(true); const profiles = b.sent.filter((x) => x.cmd !== "screenshot").map((x) => [x.cmd, x.profile]); expect(profiles).toEqual([ ["navigate", "test-qian"], ["resolve", "test-qian"], ["act", "test-qian"], ["navigate", "test-buster"], ["resolve", "test-buster"], ["act", "test-buster"], ["verify", "test-qian"], ]); });
it("each profile has its own tab: a tab one profile opened is never used for the other", async () => { const b = bridge(); await runScript(script(GIFT), b.deps); const tabOf = (cmd: string, profile: string) => b.sent.filter((x) => x.cmd === cmd && x.profile === profile).map((x) => x.tab); expect(tabOf("navigate", "test-qian")).toEqual([undefined]); // start: a new tab expect(tabOf("navigate", "test-buster")).toEqual([undefined]); // buster has no tab yet: a new one in its own Chrome expect(tabOf("resolve", "test-qian")).toEqual([7]); expect(tabOf("resolve", "test-buster")).toEqual([21]); expect(tabOf("verify", "test-qian")).toEqual([7]); });
it("the report attributes each command to the profile that did it, and says which profiles the test used", async () => { const b = bridge(); const r = await runScript(script(GIFT), b.deps); expect(r.profiles).toEqual(["test-qian", "test-buster"]); const commands = r.steps.flatMap((s) => s.commands); expect(commands.map((c) => [c.verb, c.profile])).toEqual([["start", "test-qian"], ["click", "test-qian"], ["goto", "test-buster"], ["click", "test-buster"], ["verify", "test-qian"]]); const text = formatReport(r, "gift.aite"); expect(text).toContain("as test-buster"); });
it("a single-profile script is unchanged: no profile in any request, none in any result", async () => { const b = bridge(); const r = await runScript(script(`test "t"\n needs session a\n start http://127.0.0.1:4780/x.html\n step "s"\n click text:"Go"\n`), b.deps); expect(b.sent.every((x) => !("profile" in x))).toBe(true); expect(r.profiles).toBeUndefined(); expect(r.steps.flatMap((s) => s.commands).every((c) => c.profile === undefined)).toBe(true); });
it("the start page opens in the default profile, and the other profile's first page is its own goto", async () => { const b = bridge(); await runScript(script(GIFT), b.deps); const navs = b.sent.filter((x) => x.cmd === "navigate").map((x) => [x.profile, x.url]); expect(navs).toEqual([["test-qian", "http://127.0.0.1:4780/gift.html"], ["test-buster", "http://127.0.0.1:4780/claim.html"]]); });
it("P5-6.b: one profile going away fails only its own command, as connection-lost, and the other profile was never touched", async () => { const b = bridge((body) => (body.profile === "test-buster" && body.cmd === "resolve" ? { ok: false, reason: "connection-lost", detail: { message: "the extension of test-buster disconnected" } } : undefined)); const r = await runScript(script(GIFT), b.deps); expect(r.ok).toBe(false); const failed = r.steps.flatMap((s) => s.commands).filter((c) => c.status === "failed"); expect(failed).toHaveLength(1); expect(failed[0]).toMatchObject({ verb: "click", profile: "test-buster", reason: "connection-lost" }); // what profile A did before is intact and still passed const byName = (n: string) => r.steps.find((st) => st.name === n)!; expect(byName("qian makes a gift").status).toBe("passed"); expect(byName("buster claims it").status).toBe("failed"); expect(byName("qian sees it claimed").status).toBe("skipped"); expect(formatReport(r, "gift.aite")).toMatch(/connection-lost/); });
it("a failure on the other profile is reported with that profile, and its diagnosis is asked for it", async () => { const asked: Array<{ profile?: string | undefined }> = []; const b = bridge((body) => (body.profile === "test-buster" && body.cmd === "act" ? { ok: false, reason: "not-found", detail: {} } : undefined)); b.deps.diagnose = async (f) => { asked.push({ profile: f.profile }); throw new Error("no diagnosis in this test"); }; const r = await runScript(script(GIFT), b.deps); expect(asked).toEqual([{ profile: "test-buster" }]); expect(r.steps.find((st) => st.name === "buster claims it")!.failure).toMatchObject({ verb: "click" }); expect(formatReport(r, "gift.aite")).toContain("test-buster"); });
it("the lock file keeps what each profile saw apart: the same target on two profiles is two entries", async () => { const lock = new LockStore(undefined, () => "2026-10-11T00:00:00Z"); const b = bridge(); await runScript(script(`test "t"\n needs session a\n needs session b\n start http://127.0.0.1:4780/x.html\n step "both look"\n click text:"Go"\n click text:"Go" on:b\n`), { ...b.deps, lock }); const sources = Object.values(lock.toFile().targets).map((e) => e.source).sort(); expect(sources).toEqual(['text:"Go"', 'text:"Go" on:b']); });
it("checks run for the profile of their command", async () => { const calls: Array<{ profile?: string | undefined }> = []; const b = bridge(); b.deps.runCheck = async (call) => (calls.push({ profile: call.profile }), { ok: true }); await runScript(script(`test "t"\n needs session a\n needs session b\n start http://127.0.0.1:4780/x.html\n step "s"\n call session-check on:b\n call session-check\n`), b.deps); expect(calls).toEqual([{ profile: "b" }, { profile: "a" }]); });
it("the step's picture is of the profile that did the last thing in it", async () => { const b = bridge(); b.deps.saveScreenshot = async () => undefined; await runScript(script(GIFT), b.deps); const shots = b.all.filter((x) => x.body.cmd === "screenshot").map((x) => [x.body.profile, x.body.tab]); expect(shots).toEqual([["test-qian", 7], ["test-qian", 7], ["test-buster", 21], ["test-qian", 7]]); });});