/// import type { Locator, Page } from "playwright"; /** * Gestures that do not go through CDP mouse/keyboard Input. * * Headed Chrome treats Input.dispatchMouseEvent / insertText as a reason to become the * frontmost app, which yanks OS focus out of the editor. HTMLElement.click() and * setting input.value stay inside the page. * * Evaluate callbacks must stay anonymous and must not assign inner functions to names. * tsx/esbuild keepNames wraps those as `__name(fn, "canScroll")`, and Playwright * serializes the callback into the page, where that helper does not exist. */ export async function clickWithoutOsFocus(locator: Locator, timeoutMs: number): Promise { await locator.waitFor({ state: "attached", timeout: timeoutMs }); await locator.evaluate((el) => { if (el instanceof HTMLElement) el.click(); else el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window })); }); } export async function fillWithoutOsFocus( locator: Locator, text: string, timeoutMs: number, ): Promise { await locator.waitFor({ state: "attached", timeout: timeoutMs }); await locator.evaluate((el, value) => { if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) { el.textContent = value; el.dispatchEvent(new Event("input", { bubbles: true })); return; } const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(proto, "value")?.set?.call(el, value); el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); }, text); } export async function selectWithoutOsFocus( locator: Locator, value: string, timeoutMs: number, ): Promise { await locator.waitFor({ state: "attached", timeout: timeoutMs }); const tag = await locator.evaluate((el) => el.tagName); if (tag !== "SELECT") { await locator.selectOption(value, { timeout: timeoutMs }); return; } await locator.evaluate((el, next) => { const select = el as HTMLSelectElement; select.value = next; select.dispatchEvent(new Event("input", { bubbles: true })); select.dispatchEvent(new Event("change", { bubbles: true })); }, value); } export async function scrollWithoutOsFocus( page: Page, locator: Locator | undefined, dy: number, timeoutMs: number, ): Promise { if (!locator) { await page.evaluate((delta) => window.scrollBy(0, delta), dy); return; } await locator.waitFor({ state: "attached", timeout: timeoutMs }); await locator.evaluate((el, delta) => { let target: Element | null = el; while ( target && target.scrollHeight <= target.clientHeight + 2 && target.scrollWidth <= target.clientWidth + 2 ) { target = target.parentElement; } if (target) target.scrollTop += delta; else window.scrollBy(0, delta); }, dy); }