import type { Page } from "playwright"; export type CdpClickTarget = { x: number; y: number; width: number; height: number; }; /** 在页面内解析选择器并返回视口中心坐标 */ export const RESOLVE_CLICK_TARGET = `(arg) => { var selector = arg.selector; var index = arg.index; var nodes = document.querySelectorAll(selector); var el = nodes[index]; if (!el) { return null; } el.scrollIntoView({ block: "center", inline: "nearest" }); var rect = el.getBoundingClientRect(); if (!rect.width || !rect.height) { return null; } return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, width: rect.width, height: rect.height, }; }`; async function dispatchCdpClick( page: Page, target: CdpClickTarget, ): Promise { const client = await page.context().newCDPSession(page); const x = target.x; const y = target.y; await client.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, buttons: 0, }); await client.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1, buttons: 1, }); await client.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1, buttons: 0, }); } /** * 通过 CDP Input.dispatchMouseEvent 在元素中心点击(绕过 evaluate 合成 MouseEvent)。 */ export async function cdpClickSelector( page: Page, selector: string, index = 0, ): Promise { const target = (await page.evaluate(RESOLVE_CLICK_TARGET, { selector, index, })) as CdpClickTarget | null; if (!target) { return false; } await dispatchCdpClick(page, target); return true; }