Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120import { KEYS } from "../shared/keys";import type { CdpSession } from "./cdp";
export interface PageSession { cdp: CdpSession; sessionId: string;}
/** The Chrome target of the page showing this url (and this title, if the url is shared). More than one match is refused, not guessed. */export async function findTarget(cdp: CdpSession, tab: { url: string; title?: string }): Promise<string> { const { targetInfos } = (await cdp.send("Target.getTargets")) as { targetInfos: Array<{ targetId: string; type: string; url: string; title: string }> }; let matches = targetInfos.filter((t) => t.type === "page" && t.url === tab.url); if (matches.length > 1 && tab.title !== undefined) { const byTitle = matches.filter((t) => t.title === tab.title); if (byTitle.length > 0) matches = byTitle; } if (matches.length === 0) throw new Error(`Chrome has no page at ${tab.url}`); if (matches.length > 1) throw new Error(`${matches.length} tabs show ${tab.url}, so it is not clear which one the run is using`); return matches[0]!.targetId;}
/** * Runs `work` against one tab's page over Chrome's debugging channel. The tab is found by url (and title, if the url is shared; * ambiguity is refused, not guessed), brought to the front, attached to, and detached from afterwards whatever happens. */export async function withPage<T>(cdp: CdpSession, tab: { url: string; title?: string }, work: (session: PageSession) => Promise<T>): Promise<T> { const targetId = await findTarget(cdp, tab); await cdp.send("Target.activateTarget", { targetId }); // a tab behind another may not be painted const { sessionId } = (await cdp.send("Target.attachToTarget", { targetId, flatten: true })) as { sessionId?: string }; if (!sessionId) throw new Error("Chrome would not attach to the page"); try { return await work({ cdp, sessionId }); } finally { await cdp.send("Target.detachFromTarget", { sessionId }).catch(() => undefined); }}
/** * A real click: mouse moved, button pressed, button released, at a point in the page's viewport (CSS pixels). Chrome treats * input sent this way as the user's own, so the page gets user activation and may open a pop-up; a click made by script in the page does not. */export async function trustedClick(session: PageSession, x: number, y: number): Promise<void> { if (![x, y].every((n) => Number.isFinite(n) && n >= 0)) throw new Error(`cannot click at the point (${x}, ${y})`); const send = (params: object) => session.cdp.send("Input.dispatchMouseEvent", params, session.sessionId); await send({ type: "mouseMoved", x, y }); await send({ type: "mousePressed", x, y, button: "left", buttons: 1, clickCount: 1 }); await send({ type: "mouseReleased", x, y, button: "left", buttons: 0, clickCount: 1 });}
/** A double click: two press and release pairs at one point, counted 1 and 2, as Chrome's own input counts them. */export async function trustedDoubleClick(session: PageSession, x: number, y: number): Promise<void> { if (![x, y].every((n) => Number.isFinite(n) && n >= 0)) throw new Error(`cannot click at the point (${x}, ${y})`); const send = (params: object) => session.cdp.send("Input.dispatchMouseEvent", params, session.sessionId); await send({ type: "mouseMoved", x, y }); for (const clickCount of [1, 2]) { await send({ type: "mousePressed", x, y, button: "left", buttons: 1, clickCount }); await send({ type: "mouseReleased", x, y, button: "left", buttons: 0, clickCount }); }}
/** Selects everything in the focused field (a key press that carries the editing command), so the next typing replaces it. */export async function trustedSelectAll(session: PageSession): Promise<void> { await session.cdp.send("Input.dispatchKeyEvent", { type: "rawKeyDown", key: "a", code: "KeyA", windowsVirtualKeyCode: 65, commands: ["selectAll"] }, session.sessionId); await session.cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "a", code: "KeyA", windowsVirtualKeyCode: 65 }, session.sessionId);}
/** One key press by its script name (Enter, Tab, Escape, ArrowDown, ...) or a single character. */export async function trustedPress(session: PageSession, name: string): Promise<void> { const spec = KEYS[name]; if (spec === undefined && [...name].length !== 1) throw new Error(`unknown key ${JSON.stringify(name)}; use one of ${Object.keys(KEYS).join(", ")} or a single character`); const send = (params: object) => session.cdp.send("Input.dispatchKeyEvent", params, session.sessionId); if (spec === undefined) { await send({ type: "keyDown", key: name, text: name, unmodifiedText: name }); await send({ type: "keyUp", key: name }); return; } await send({ type: "keyDown", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.vk, ...(spec.text !== undefined ? { text: spec.text } : {}) }); await send({ type: "keyUp", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.vk });}
const DRAG_STEPS = 8;
/** Presses on one point, drags to another in small steps with the button held (so the page sees a drag, not a jump), and releases. */export async function trustedDrag(session: PageSession, from: { x: number; y: number }, to: { x: number; y: number }): Promise<void> { if (![from.x, from.y, to.x, to.y].every((n) => Number.isFinite(n) && n >= 0)) throw new Error(`cannot drag from (${from.x}, ${from.y}) to (${to.x}, ${to.y})`); const send = (params: object) => session.cdp.send("Input.dispatchMouseEvent", params, session.sessionId); await send({ type: "mouseMoved", x: from.x, y: from.y }); await send({ type: "mousePressed", x: from.x, y: from.y, button: "left", buttons: 1, clickCount: 1 }); for (let i = 1; i <= DRAG_STEPS; i++) { const x = from.x + ((to.x - from.x) * i) / DRAG_STEPS; const y = from.y + ((to.y - from.y) * i) / DRAG_STEPS; await send({ type: "mouseMoved", x, y, button: "left", buttons: 1 }); } await send({ type: "mouseReleased", x: to.x, y: to.y, button: "left", buttons: 0, clickCount: 1 });}
/** * Types into whatever has focus, as key presses: keydown, input and keyup fire as for a person, so a framework that tracks its own * copy of an input's value (React) sees the change. Characters that are not plain ASCII are inserted as text instead. */export async function trustedType(session: PageSession, text: string): Promise<void> { let index = 0; for (const ch of text) { try { if (ch === "\n") { await session.cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, text: "\r" }, session.sessionId); await session.cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 }, session.sessionId); } else if (ch >= " " && ch <= "~") { await session.cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: ch, text: ch, unmodifiedText: ch }, session.sessionId); await session.cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: ch }, session.sessionId); } else { await session.cdp.send("Input.insertText", { text: ch }, session.sessionId); } } catch (err) { throw new Error(`could not type character ${index + 1} (${ch}): ${err instanceof Error ? err.message : String(err)}`); } index++; }}