///
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);
}