Skip to content
This repository was archived by the owner on Apr 17, 2026. It is now read-only.

Commit 1e86fd0

Browse files
fry-lobsterclaude
andcommitted
feat: add mousedown and mouseup commands for press-and-hold interactions
Separates the mousePressed/mouseReleased CDP events that click bundles together, enabling press-and-hold patterns needed for CAPTCHA solving (e.g. Arkose Labs). Follows the same pattern as keydown/keyup. Supports ref, CSS selector, nodeId, and x/y coordinate targeting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ce7efa1 commit 1e86fd0

6 files changed

Lines changed: 133 additions & 0 deletions

File tree

cmd/pinchtab/cmd_cli_browser_actions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ var keydownCmd = newSimpleActionCmd("keydown <key>", "Hold a key down", "keydown
7979

8080
var keyupCmd = newSimpleActionCmd("keyup <key>", "Release a key", "keyup", cobra.ExactArgs(1))
8181

82+
var mousedownCmd = newOptionalRefActionCmd("mousedown [ref]", "Press mouse button down", "mousedown")
83+
84+
var mouseupCmd = newOptionalRefActionCmd("mouseup [ref]", "Release mouse button", "mouseup")
85+
8286
var scrollintoviewCmd = newOptionalRefActionCmd("scrollintoview <selector>", "Scroll element into view and return bounding box", "scrollintoview")
8387

8488
var dialogCmd = &cobra.Command{

cmd/pinchtab/cmd_cli_register.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ func registerBrowserCommands() {
3939
keyboardCmd,
4040
keydownCmd,
4141
keyupCmd,
42+
mousedownCmd,
43+
mouseupCmd,
4244
scrollintoviewCmd,
4345
dialogCmd,
4446
consoleCmd,
@@ -84,6 +86,8 @@ func registerBrowserCommands() {
8486
keyboardCmd,
8587
keydownCmd,
8688
keyupCmd,
89+
mousedownCmd,
90+
mouseupCmd,
8791
scrollintoviewCmd,
8892
dialogCmd,
8993
consoleCmd,
@@ -185,6 +189,8 @@ func configureBrowserFlags() {
185189
keyboardInsertTextCmd,
186190
keydownCmd,
187191
keyupCmd,
192+
mousedownCmd,
193+
mouseupCmd,
188194
scrollintoviewCmd,
189195
networkCmd,
190196
waitCmd,

internal/bridge/action_pointer.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,40 @@ func (b *Bridge) actionHover(ctx context.Context, req ActionRequest) (map[string
6060
return nil, fmt.Errorf("need selector, ref, nodeId, or x/y coordinates")
6161
}
6262

63+
func (b *Bridge) actionMouseDown(ctx context.Context, req ActionRequest) (map[string]any, error) {
64+
if req.NodeID > 0 {
65+
return map[string]any{"mousedown": true}, MouseDownByNodeID(ctx, req.NodeID)
66+
}
67+
if req.Selector != "" {
68+
node, err := firstNodeBySelector(ctx, req.Selector)
69+
if err != nil {
70+
return nil, err
71+
}
72+
return map[string]any{"mousedown": true}, MouseDownByNodeID(ctx, int64(node.BackendNodeID))
73+
}
74+
if req.HasXY {
75+
return map[string]any{"mousedown": true, "x": req.X, "y": req.Y}, MouseDownByCoordinate(ctx, req.X, req.Y)
76+
}
77+
return nil, fmt.Errorf("need selector, ref, nodeId, or x/y coordinates")
78+
}
79+
80+
func (b *Bridge) actionMouseUp(ctx context.Context, req ActionRequest) (map[string]any, error) {
81+
if req.NodeID > 0 {
82+
return map[string]any{"mouseup": true}, MouseUpByNodeID(ctx, req.NodeID)
83+
}
84+
if req.Selector != "" {
85+
node, err := firstNodeBySelector(ctx, req.Selector)
86+
if err != nil {
87+
return nil, err
88+
}
89+
return map[string]any{"mouseup": true}, MouseUpByNodeID(ctx, int64(node.BackendNodeID))
90+
}
91+
if req.HasXY {
92+
return map[string]any{"mouseup": true, "x": req.X, "y": req.Y}, MouseUpByCoordinate(ctx, req.X, req.Y)
93+
}
94+
return nil, fmt.Errorf("need selector, ref, nodeId, or x/y coordinates")
95+
}
96+
6397
func (b *Bridge) actionScroll(ctx context.Context, req ActionRequest) (map[string]any, error) {
6498
if req.NodeID > 0 {
6599
return map[string]any{"scrolled": true}, ScrollByNodeID(ctx, req.NodeID)

internal/bridge/action_registry.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const (
1919
ActionKeyboardInsert = "keyboard-inserttext"
2020
ActionKeyDown = "keydown"
2121
ActionKeyUp = "keyup"
22+
ActionMouseDown = "mousedown"
23+
ActionMouseUp = "mouseup"
2224
ActionScrollIntoView = "scrollintoview"
2325
)
2426

@@ -42,6 +44,8 @@ func (b *Bridge) InitActionRegistry() {
4244
ActionKeyboardInsert: b.actionKeyboardInsert,
4345
ActionKeyDown: b.actionKeyDown,
4446
ActionKeyUp: b.actionKeyUp,
47+
ActionMouseDown: b.actionMouseDown,
48+
ActionMouseUp: b.actionMouseUp,
4549
ActionScrollIntoView: b.actionScrollIntoView,
4650
}
4751
}

internal/bridge/cdpops/pointer.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,75 @@ func DragByNodeID(ctx context.Context, nodeID int64, dx, dy int) error {
188188
)
189189
}
190190

191+
func MouseDownByCoordinate(ctx context.Context, x, y float64) error {
192+
if x < 0 || y < 0 {
193+
return fmt.Errorf("x/y coordinates must be >= 0")
194+
}
195+
196+
return chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
197+
return chromedp.FromContext(ctx).Target.Execute(ctx, "Input.dispatchMouseEvent", map[string]any{
198+
"type": "mousePressed",
199+
"button": "left",
200+
"clickCount": 1,
201+
"x": x,
202+
"y": y,
203+
}, nil)
204+
}))
205+
}
206+
207+
func MouseDownByNodeID(ctx context.Context, nodeID int64) error {
208+
x, y, err := GetElementCenter(ctx, nodeID)
209+
if err != nil {
210+
return err
211+
}
212+
213+
return chromedp.Run(ctx,
214+
chromedp.ActionFunc(func(ctx context.Context) error {
215+
return chromedp.FromContext(ctx).Target.Execute(ctx, "DOM.scrollIntoViewIfNeeded", map[string]any{"backendNodeId": nodeID}, nil)
216+
}),
217+
chromedp.ActionFunc(func(ctx context.Context) error {
218+
return chromedp.FromContext(ctx).Target.Execute(ctx, "Input.dispatchMouseEvent", map[string]any{
219+
"type": "mousePressed",
220+
"button": "left",
221+
"clickCount": 1,
222+
"x": x, "y": y,
223+
}, nil)
224+
}),
225+
)
226+
}
227+
228+
func MouseUpByCoordinate(ctx context.Context, x, y float64) error {
229+
if x < 0 || y < 0 {
230+
return fmt.Errorf("x/y coordinates must be >= 0")
231+
}
232+
233+
return chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
234+
return chromedp.FromContext(ctx).Target.Execute(ctx, "Input.dispatchMouseEvent", map[string]any{
235+
"type": "mouseReleased",
236+
"button": "left",
237+
"clickCount": 1,
238+
"x": x,
239+
"y": y,
240+
}, nil)
241+
}))
242+
}
243+
244+
func MouseUpByNodeID(ctx context.Context, nodeID int64) error {
245+
x, y, err := GetElementCenter(ctx, nodeID)
246+
if err != nil {
247+
return err
248+
}
249+
250+
return chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
251+
return chromedp.FromContext(ctx).Target.Execute(ctx, "Input.dispatchMouseEvent", map[string]any{
252+
"type": "mouseReleased",
253+
"button": "left",
254+
"clickCount": 1,
255+
"x": x, "y": y,
256+
}, nil)
257+
}))
258+
}
259+
191260
func HoverByCoordinate(ctx context.Context, x, y float64) error {
192261
if x < 0 || y < 0 {
193262
return fmt.Errorf("x/y coordinates must be >= 0")

internal/bridge/cdpops_facade.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ func DragByNodeID(ctx context.Context, nodeID int64, dx, dy int) error {
6363
return bridgecdpops.DragByNodeID(ctx, nodeID, dx, dy)
6464
}
6565

66+
func MouseDownByCoordinate(ctx context.Context, x, y float64) error {
67+
return bridgecdpops.MouseDownByCoordinate(ctx, x, y)
68+
}
69+
70+
func MouseDownByNodeID(ctx context.Context, nodeID int64) error {
71+
return bridgecdpops.MouseDownByNodeID(ctx, nodeID)
72+
}
73+
74+
func MouseUpByCoordinate(ctx context.Context, x, y float64) error {
75+
return bridgecdpops.MouseUpByCoordinate(ctx, x, y)
76+
}
77+
78+
func MouseUpByNodeID(ctx context.Context, nodeID int64) error {
79+
return bridgecdpops.MouseUpByNodeID(ctx, nodeID)
80+
}
81+
6682
func HoverByCoordinate(ctx context.Context, x, y float64) error {
6783
return bridgecdpops.HoverByCoordinate(ctx, x, y)
6884
}

0 commit comments

Comments
 (0)