fix(input): generate proper multi-click events

This commit is contained in:
Peter Steinberger 2025-12-18 00:41:57 +00:00
parent 3b57ded2c4
commit c596eddb9e
2 changed files with 101 additions and 34 deletions

View File

@ -32,42 +32,67 @@ extension Element {
/// Click at a specific point on screen
@MainActor public static func clickAt(_ point: CGPoint, button: MouseButton = .left, clickCount: Int = 1) throws {
// Create mouse down event
guard let mouseDown = CGEvent(
mouseEventSource: nil,
mouseType: button == .left ? .leftMouseDown : .rightMouseDown,
mouseCursorPosition: point,
mouseButton: button == .left ? .left : .right)
else {
throw UIAutomationError.failedToCreateEvent
let clickPairs = try self.buildClickEventPairs(at: point, button: button, clickCount: clickCount)
for (index, pair) in clickPairs.enumerated() {
pair.down.post(tap: .cghidEventTap)
// Small delay between down and up
Thread.sleep(forTimeInterval: 0.01)
pair.up.post(tap: .cghidEventTap)
// Small delay between successive clicks (stay within the system double-click interval)
if index < clickPairs.count - 1 {
Thread.sleep(forTimeInterval: 0.03)
}
}
}
@MainActor
internal static func buildClickEventPairs(
at point: CGPoint,
button: MouseButton,
clickCount: Int) throws -> [(down: CGEvent, up: CGEvent)]
{
let clampedCount = max(1, clickCount)
let downType: CGEventType = (button == .left ? .leftMouseDown : .rightMouseDown)
let upType: CGEventType = (button == .left ? .leftMouseUp : .rightMouseUp)
let mouseButton: CGMouseButton = (button == .left ? .left : .right)
var pairs: [(down: CGEvent, up: CGEvent)] = []
pairs.reserveCapacity(clampedCount)
for clickIndex in 1...clampedCount {
guard let mouseDown = CGEvent(
mouseEventSource: nil,
mouseType: downType,
mouseCursorPosition: point,
mouseButton: mouseButton)
else {
throw UIAutomationError.failedToCreateEvent
}
guard let mouseUp = CGEvent(
mouseEventSource: nil,
mouseType: upType,
mouseCursorPosition: point,
mouseButton: mouseButton)
else {
throw UIAutomationError.failedToCreateEvent
}
// For a double click, the system expects a sequence of click states:
// (1) down/up with clickState=1, then (2) down/up with clickState=2.
let clickState = Int64(clickIndex)
mouseDown.setIntegerValueField(.mouseEventClickState, value: clickState)
mouseUp.setIntegerValueField(.mouseEventClickState, value: clickState)
pairs.append((down: mouseDown, up: mouseUp))
}
// Set click count
mouseDown.setIntegerValueField(.mouseEventClickState, value: Int64(clickCount))
// Create mouse up event
guard let mouseUp = CGEvent(
mouseEventSource: nil,
mouseType: button == .left ? .leftMouseUp : .rightMouseUp,
mouseCursorPosition: point,
mouseButton: button == .left ? .left : .right)
else {
throw UIAutomationError.failedToCreateEvent
}
// Set click count
mouseUp.setIntegerValueField(.mouseEventClickState, value: Int64(clickCount))
// Post events
mouseDown.post(tap: .cghidEventTap)
// Small delay between down and up
Thread.sleep(forTimeInterval: 0.01)
mouseUp.post(tap: .cghidEventTap)
// Note: clickCount=2 events are automatically handled by the system
// No need to post additional events for double clicks
return pairs
}
/// Wait for this element to become actionable

View File

@ -0,0 +1,42 @@
import CoreGraphics
import Testing
@testable import AXorcist
@Suite("Click event generation")
struct ClickEventGenerationTests {
@Test("Single click uses clickState=1")
@MainActor
func singleClickUsesClickStateOne() throws {
let pairs = try Element.buildClickEventPairs(
at: CGPoint(x: 10, y: 20),
button: .left,
clickCount: 1)
#expect(pairs.count == 1)
#expect(pairs[0].down.type == .leftMouseDown)
#expect(pairs[0].up.type == .leftMouseUp)
#expect(pairs[0].down.getIntegerValueField(.mouseEventClickState) == 1)
#expect(pairs[0].up.getIntegerValueField(.mouseEventClickState) == 1)
}
@Test("Double click emits clickState sequence 1 then 2")
@MainActor
func doubleClickUsesSequentialClickStates() throws {
let pairs = try Element.buildClickEventPairs(
at: CGPoint(x: 10, y: 20),
button: .left,
clickCount: 2)
#expect(pairs.count == 2)
#expect(pairs[0].down.type == .leftMouseDown)
#expect(pairs[0].up.type == .leftMouseUp)
#expect(pairs[0].down.getIntegerValueField(.mouseEventClickState) == 1)
#expect(pairs[0].up.getIntegerValueField(.mouseEventClickState) == 1)
#expect(pairs[1].down.type == .leftMouseDown)
#expect(pairs[1].up.type == .leftMouseUp)
#expect(pairs[1].down.getIntegerValueField(.mouseEventClickState) == 2)
#expect(pairs[1].up.getIntegerValueField(.mouseEventClickState) == 2)
}
}