From 32d6f0c74b7813ba012d5c5c199d8326cc86197f Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:41:49 +0800 Subject: [PATCH] fix(controller): click action robust --- packages/page-controller/src/actions.ts | 58 ++++++++++++++++--------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/page-controller/src/actions.ts b/packages/page-controller/src/actions.ts index 92c7388..80fc73f 100644 --- a/packages/page-controller/src/actions.ts +++ b/packages/page-controller/src/actions.ts @@ -42,19 +42,20 @@ let lastClickedElement: HTMLElement | null = null function blurLastClickedElement() { if (lastClickedElement) { + lastClickedElement.dispatchEvent(new PointerEvent('pointerout', { bubbles: true })) + lastClickedElement.dispatchEvent(new PointerEvent('pointerleave', { bubbles: false })) + lastClickedElement.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })) + lastClickedElement.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false })) lastClickedElement.blur() - lastClickedElement.dispatchEvent( - new MouseEvent('mouseout', { bubbles: true, cancelable: true }) - ) - lastClickedElement.dispatchEvent( - new MouseEvent('mouseleave', { bubbles: false, cancelable: true }) - ) lastClickedElement = null } } /** - * Simulate a click on the element + * Simulate a full click following W3C Pointer Events + UI Events spec order: + * pointerover/enter → mouseover/enter → pointerdown → mousedown → [focus] → + * pointerup → mouseup → click + * * @private Internal method, subject to change at any time. */ export async function clickElement(element: HTMLElement) { @@ -63,7 +64,6 @@ export async function clickElement(element: HTMLElement) { lastClickedElement = element await scrollIntoViewIfNeeded(element) - // Scroll the iframe element itself into view if needed const frame = element.ownerDocument.defaultView?.frameElement if (frame) await scrollIntoViewIfNeeded(frame) @@ -72,23 +72,41 @@ export async function clickElement(element: HTMLElement) { await waitFor(0.1) - // hover it - element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true })) - element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true })) + const rect = element.getBoundingClientRect() + const x = rect.left + rect.width / 2 + const y = rect.top + rect.height / 2 + const pointerOpts = { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + pointerType: 'mouse' as const, + } + const mouseOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 } - // dispatch a sequence of events to ensure all listeners are triggered - element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })) + // 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 })) - // focus it to ensure it gets the click event - element.focus() + // Press + element.dispatchEvent(new PointerEvent('pointerdown', pointerOpts)) + element.dispatchEvent(new MouseEvent('mousedown', mouseOpts)) - element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })) - element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) + // Focus is not part of the standard "undefined and varies between user agents". + // Browsers implicitly focus focusable elements on mousedown as an internal behavior. + element.focus({ preventScroll: true }) - // dispatch a click event - // element.click() + // Release + element.dispatchEvent(new PointerEvent('pointerup', pointerOpts)) + element.dispatchEvent(new MouseEvent('mouseup', mouseOpts)) - await waitFor(0.2) // Wait to ensure click event processing completes + // Click — element.click() triggers default behaviors (e.g. navigation, + // form submission) that dispatchEvent(new MouseEvent('click')) may not. + element.click() + + await waitFor(0.2) } /**