feat(controller): enhance click action with elementFromPoint

This commit is contained in:
Simon
2026-03-31 20:02:39 +08:00
parent 8eee3b27e2
commit 296459924a
3 changed files with 45 additions and 16 deletions

View File

@@ -218,6 +218,7 @@ export class PageController extends EventTarget {
* Clean up all element highlights
*/
async cleanUpHighlights(): Promise<void> {
console.log('[PageController] cleanUpHighlights')
dom.cleanUpHighlights()
}

View File

@@ -4,6 +4,9 @@
*/
import type { InteractiveElementDomNode } from './dom/dom_tree/type'
import {
clickPointer,
disablePassThrough,
enablePassThrough,
getNativeValueSetter,
isHTMLElement,
isInputElement,
@@ -68,43 +71,56 @@ export async function clickElement(element: HTMLElement) {
if (frame) await scrollIntoViewIfNeeded(frame)
await movePointerToElement(element)
window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer'))
await clickPointer()
await waitFor(0.1)
const rect = element.getBoundingClientRect()
const x = rect.left + rect.width / 2
const y = rect.top + rect.height / 2
// Hit-test to find the deepest element at click coordinates, matching
// real browser behavior where events target the innermost element.
// @note This may hit a element in the blacklist
// TODO: This is a temporary workaround. Should have been handled during dom extraction.
const doc = element.ownerDocument
await enablePassThrough()
const hitTarget = doc.elementFromPoint(x, y)
await disablePassThrough()
const target =
hitTarget instanceof HTMLElement && element.contains(hitTarget) ? hitTarget : element
const pointerOpts = {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
pointerType: 'mouse' as const,
pointerType: 'mouse',
}
const mouseOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }
// Hover — pointer events first, then mouse events (spec order)
element.dispatchEvent(new PointerEvent('pointerover', pointerOpts))
element.dispatchEvent(new PointerEvent('pointerenter', { ...pointerOpts, bubbles: false }))
element.dispatchEvent(new MouseEvent('mouseover', mouseOpts))
element.dispatchEvent(new MouseEvent('mouseenter', { ...mouseOpts, bubbles: false }))
target.dispatchEvent(new PointerEvent('pointerover', pointerOpts))
target.dispatchEvent(new PointerEvent('pointerenter', { ...pointerOpts, bubbles: false }))
target.dispatchEvent(new MouseEvent('mouseover', mouseOpts))
target.dispatchEvent(new MouseEvent('mouseenter', { ...mouseOpts, bubbles: false }))
// Press
element.dispatchEvent(new PointerEvent('pointerdown', pointerOpts))
element.dispatchEvent(new MouseEvent('mousedown', mouseOpts))
target.dispatchEvent(new PointerEvent('pointerdown', pointerOpts))
target.dispatchEvent(new MouseEvent('mousedown', mouseOpts))
// Focus is not part of the standard "undefined and varies between user agents".
// Browsers implicitly focus focusable elements on mousedown as an internal behavior.
// Focus is not part of the standard pointer/mouse event sequence
// "undefined and varies between user agents".
// We focus the original element (nearest focusable ancestor), not the hit-test target, matching browser behavior.
element.focus({ preventScroll: true })
// Release
element.dispatchEvent(new PointerEvent('pointerup', pointerOpts))
element.dispatchEvent(new MouseEvent('mouseup', mouseOpts))
target.dispatchEvent(new PointerEvent('pointerup', pointerOpts))
target.dispatchEvent(new MouseEvent('mouseup', mouseOpts))
// Click — element.click() triggers default behaviors (e.g. <a> navigation,
// form submission) that dispatchEvent(new MouseEvent('click')) may not.
element.click()
// Click — activation behavior (navigation, form submit, etc.) triggers
// via bubbling from target up to the interactive ancestor.
target.click()
await waitFor(0.2)
}

View File

@@ -48,7 +48,7 @@ export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
// ======= dom utils =======
// ======= mask events =======
export async function movePointerToElement(element: HTMLElement) {
const rect = element.getBoundingClientRect()
@@ -60,3 +60,15 @@ export async function movePointerToElement(element: HTMLElement) {
await waitFor(0.3)
}
export async function clickPointer() {
window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer'))
}
export async function enablePassThrough() {
window.dispatchEvent(new CustomEvent('PageAgent::EnablePassThrough'))
}
export async function disablePassThrough() {
window.dispatchEvent(new CustomEvent('PageAgent::DisablePassThrough'))
}