Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190import { describe, expect, it } from "vitest";import { trustedClick, trustedDoubleClick, trustedDrag, trustedPress, trustedSelectAll, trustedType, withPage } from "../../runner/input";import { allowDownloads } from "../../runner/downloadSetup";import type { CdpSession } from "../../runner/cdp";
type Call = { method: string; params: any; sessionId: string | undefined };function fakeCdp(targets = [{ targetId: "T1", type: "page", url: "http://x/", title: "X" }], fail: string | undefined = undefined) { const calls: Call[] = []; const cdp: CdpSession = { send: async (method, params, sessionId) => { calls.push({ method, params, sessionId }); if (fail && method === fail) throw new Error(`${method}: refused`); if (method === "Target.getTargets") return { targetInfos: targets }; if (method === "Target.attachToTarget") return { sessionId: "S1" }; return {}; }, }; return { cdp, calls, methods: () => calls.map((c) => c.method) };}const session = (f: ReturnType<typeof fakeCdp>) => ({ cdp: f.cdp, sessionId: "S1" });
describe("withPage", () => { it("finds the tab's page, brings it to the front, attaches, runs the work in that session, and detaches", async () => { const f = fakeCdp(); const result = await withPage(f.cdp, { url: "http://x/" }, async (s) => { expect(s.sessionId).toBe("S1"); await s.cdp.send("Runtime.evaluate", { expression: "1" }, s.sessionId); return "done"; }); expect(result).toBe("done"); expect(f.methods()).toEqual(["Target.getTargets", "Target.activateTarget", "Target.attachToTarget", "Runtime.evaluate", "Target.detachFromTarget"]); expect(f.calls[3]!.sessionId).toBe("S1"); }); it("detaches even when the work throws, and passes the error on", async () => { const f = fakeCdp(); await expect(withPage(f.cdp, { url: "http://x/" }, async () => { throw new Error("boom"); })).rejects.toThrowError("boom"); expect(f.methods().at(-1)).toBe("Target.detachFromTarget"); }); it("says so when no page has the url, and does not attach", async () => { const f = fakeCdp(); await expect(withPage(f.cdp, { url: "http://nowhere/" }, async () => 1)).rejects.toThrowError("Chrome has no page at http://nowhere/"); expect(f.methods()).toEqual(["Target.getTargets"]); }); it("refuses an ambiguous tab", async () => { const f = fakeCdp([{ targetId: "A", type: "page", url: "http://x/", title: "S" }, { targetId: "B", type: "page", url: "http://x/", title: "S" }]); await expect(withPage(f.cdp, { url: "http://x/", title: "S" }, async () => 1)).rejects.toThrowError("2 tabs show http://x/"); }); it("refuses when Chrome will not attach", async () => { const f = fakeCdp(); f.cdp.send = async (m) => (m === "Target.getTargets" ? { targetInfos: [{ targetId: "T", type: "page", url: "http://x/", title: "" }] } : m === "Target.attachToTarget" ? {} : {}); await expect(withPage(f.cdp, { url: "http://x/" }, async () => 1)).rejects.toThrowError("Chrome would not attach to the page"); });});
describe("trustedClick", () => { it("moves, presses and releases the left button at the point, as one real click", async () => { const f = fakeCdp(); await trustedClick(session(f), 120.5, 80); expect(f.calls.map((c) => c.params)).toEqual([ { type: "mouseMoved", x: 120.5, y: 80 }, { type: "mousePressed", x: 120.5, y: 80, button: "left", buttons: 1, clickCount: 1 }, { type: "mouseReleased", x: 120.5, y: 80, button: "left", buttons: 0, clickCount: 1 }, ]); expect(f.methods()).toEqual(["Input.dispatchMouseEvent", "Input.dispatchMouseEvent", "Input.dispatchMouseEvent"]); expect(f.calls.every((c) => c.sessionId === "S1")).toBe(true); }); it.each([[NaN, 1], [1, Infinity], [-1, 5], [5, -0.5]])("refuses the point (%s, %s) without sending anything", async (x, y) => { const f = fakeCdp(); await expect(trustedClick(session(f), x, y)).rejects.toThrowError(/point/); expect(f.calls).toEqual([]); });});
describe("trustedType", () => { it("presses and releases a key for each printable ASCII character, carrying its text", async () => { const f = fakeCdp(); await trustedType(session(f), "aB 1"); expect(f.calls.map((c) => [c.params.type, c.params.key, c.params.text])).toEqual([ ["keyDown", "a", "a"], ["keyUp", "a", undefined], ["keyDown", "B", "B"], ["keyUp", "B", undefined], ["keyDown", " ", " "], ["keyUp", " ", undefined], ["keyDown", "1", "1"], ["keyUp", "1", undefined], ]); expect(f.calls.every((c) => c.method === "Input.dispatchKeyEvent" && c.sessionId === "S1")).toBe(true); }); it("sends a newline as the Enter key", async () => { const f = fakeCdp(); await trustedType(session(f), "\n"); expect(f.calls[0]!.params).toMatchObject({ type: "keyDown", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, text: "\r" }); expect(f.calls[1]!.params).toMatchObject({ type: "keyUp", key: "Enter" }); }); it("inserts anything else (accents, other scripts, emoji) as text, one character at a time", async () => { const f = fakeCdp(); await trustedType(session(f), "é中\u{1F600}"); expect(f.calls.map((c) => [c.method, c.params.text])).toEqual([["Input.insertText", "é"], ["Input.insertText", "中"], ["Input.insertText", "\u{1F600}"]]); }); it("types nothing for an empty string", async () => { const f = fakeCdp(); await trustedType(session(f), ""); expect(f.calls).toEqual([]); }); it("stops at the first character Chrome refuses and says which", async () => { const f = fakeCdp(undefined, "Input.dispatchKeyEvent"); await expect(trustedType(session(f), "ab")).rejects.toThrowError("could not type character 1 (a): Input.dispatchKeyEvent: refused"); expect(f.calls).toHaveLength(1); });});
describe("allowDownloads", () => { it("tells Chrome to save downloads in the folder, and to report their progress", async () => { const f = fakeCdp(); await allowDownloads(f.cdp, "/runs/1/downloads"); expect(f.calls).toEqual([{ method: "Browser.setDownloadBehavior", params: { behavior: "allow", downloadPath: "/runs/1/downloads", eventsEnabled: true }, sessionId: undefined }]); }); it("refuses a relative folder (Chrome would resolve it against its own working folder)", async () => { const f = fakeCdp(); await expect(allowDownloads(f.cdp, "downloads")).rejects.toThrowError("download folder must be an absolute path"); expect(f.calls).toEqual([]); });});
describe("trustedDoubleClick", () => { it("is press and release with click count 1, then press and release with click count 2, at the same point", async () => { const f = fakeCdp(); await trustedDoubleClick(session(f), 40, 50); const events = f.calls.map((c) => [c.params.type, c.params.clickCount ?? null, c.params.x, c.params.y]); expect(events).toEqual([["mouseMoved", null, 40, 50], ["mousePressed", 1, 40, 50], ["mouseReleased", 1, 40, 50], ["mousePressed", 2, 40, 50], ["mouseReleased", 2, 40, 50]]); expect(f.calls.every((c) => c.method === "Input.dispatchMouseEvent" && c.sessionId === "S1")).toBe(true); }); it("refuses a point that is not on screen", async () => { await expect(trustedDoubleClick(session(fakeCdp()), -1, 5)).rejects.toThrowError("cannot click at the point"); });});
describe("trustedSelectAll", () => { it("sends the select-all editing command on a key press, then releases the key", async () => { const f = fakeCdp(); await trustedSelectAll(session(f)); expect(f.calls[0]!.params).toMatchObject({ type: "rawKeyDown", key: "a", commands: ["selectAll"] }); expect(f.calls[1]!.params).toMatchObject({ type: "keyUp", key: "a" }); });});
describe("trustedPress", () => { it("presses a named key with its virtual key code, and types the text it produces", async () => { const f = fakeCdp(); await trustedPress(session(f), "Enter"); expect(f.calls.map((c) => c.params)).toEqual([ { type: "keyDown", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, text: "\r" }, { type: "keyUp", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 }, ]); }); it("a key that types nothing has no text", async () => { const f = fakeCdp(); await trustedPress(session(f), "Tab"); expect(f.calls[0]!.params).not.toHaveProperty("text"); expect(f.calls[0]!.params).toMatchObject({ windowsVirtualKeyCode: 9 }); }); it("a single character is a plain key press", async () => { const f = fakeCdp(); await trustedPress(session(f), "x"); expect(f.calls[0]!.params).toMatchObject({ type: "keyDown", key: "x", text: "x" }); }); it("an unknown name is refused before anything is sent", async () => { const f = fakeCdp(); await expect(trustedPress(session(f), "Banana")).rejects.toThrowError(/Banana/); expect(f.calls).toEqual([]); });});
describe("trustedDrag", () => { it("presses on the start, moves across in steps with the button held, and releases on the end", async () => { const f = fakeCdp(); await trustedDrag(session(f), { x: 10, y: 20 }, { x: 110, y: 220 }); const types = f.calls.map((c) => c.params.type); expect(types[0]).toBe("mouseMoved"); expect(types[1]).toBe("mousePressed"); expect(types.at(-1)).toBe("mouseReleased"); const moves = f.calls.slice(2, -1); expect(moves.length).toBeGreaterThanOrEqual(5); expect(moves.every((c) => c.params.type === "mouseMoved" && c.params.buttons === 1)).toBe(true); expect(f.calls.at(-1)!.params).toMatchObject({ x: 110, y: 220 }); const xs = moves.map((c) => c.params.x as number); expect([...xs].sort((a, b) => a - b)).toEqual(xs); }); it("refuses a point that is not on screen", async () => { await expect(trustedDrag(session(fakeCdp()), { x: 1, y: 1 }, { x: -5, y: 3 })).rejects.toThrowError("cannot drag"); });});