fix(chrome): route action methods through runCtx so caller cancellation aborts CDP

This commit is contained in:
pj committed 2026-06-06 13:05:34 +05:30
1 parent 6f69816ca5
commit ee0716a2bf
2 files changed
+71 -15

No files matched your search

+29 -15
View File
@@ -131,13 +131,17 @@ func (d *Driver) Terminate(_ context.Context) error {
return nil
}
func (d *Driver) Tap(_ context.Context, x, y int) error {
return chromedp.Run(d.tabCtx,
func (d *Driver) Tap(ctx context.Context, x, y int) error {
runCtx, cancel := d.runCtx(ctx)
defer cancel()
return chromedp.Run(runCtx,
chromedp.MouseClickXY(float64(x), float64(y)),
)
}
func (d *Driver) TapSelector(_ context.Context, selector string) error {
func (d *Driver) TapSelector(ctx context.Context, selector string) error {
runCtx, cancel := d.runCtx(ctx)
defer cancel()
target, isXPath, err := TranslateStringSelector(selector)
if err != nil {
// Fall back to passing the string straight through; chromedp will
@@ -145,9 +149,9 @@ func (d *Driver) TapSelector(_ context.Context, selector string) error {
target = selector
}
if isXPath {
return chromedp.Run(d.tabCtx, chromedp.Click(target, chromedp.NodeVisible, chromedp.BySearch))
return chromedp.Run(runCtx, chromedp.Click(target, chromedp.NodeVisible, chromedp.BySearch))
}
return chromedp.Run(d.tabCtx, chromedp.Click(target, chromedp.NodeVisible))
return chromedp.Run(runCtx, chromedp.Click(target, chromedp.NodeVisible))
}
// doubleTapGap is the inter-tap delay for DoubleTap: short enough to land both
@@ -177,8 +181,10 @@ func webDoubleTap(ctx context.Context, tap func() error) error {
return tap()
}
func (d *Driver) InputText(_ context.Context, text string) error {
return chromedp.Run(d.tabCtx,
func (d *Driver) InputText(callerCtx context.Context, text string) error {
runCtx, cancel := d.runCtx(callerCtx)
defer cancel()
return chromedp.Run(runCtx,
chromedp.ActionFunc(func(ctx context.Context) error {
// Select any existing content so InsertText replaces rather than appends.
if err := chromedp.Evaluate(`
@@ -201,8 +207,10 @@ func (d *Driver) ReplacesTextOnInput() bool {
// EraseText clears the focused field. InputText above already replaces via
// select-all, so the character count is not needed to bound the deletion.
func (d *Driver) EraseText(_ context.Context, _ int) error {
return chromedp.Run(d.tabCtx,
func (d *Driver) EraseText(callerCtx context.Context, _ int) error {
runCtx, cancel := d.runCtx(callerCtx)
defer cancel()
return chromedp.Run(runCtx,
chromedp.ActionFunc(func(ctx context.Context) error {
if err := chromedp.Evaluate(`
(function() {
@@ -216,7 +224,9 @@ func (d *Driver) EraseText(_ context.Context, _ int) error {
)
}
func (d *Driver) Swipe(_ context.Context, fromX, fromY, toX, toY int, duration time.Duration) error {
func (d *Driver) Swipe(ctx context.Context, fromX, fromY, toX, toY int, duration time.Duration) error {
runCtx, cancel := d.runCtx(ctx)
defer cancel()
millis := max(duration.Milliseconds(), 50)
script := fmt.Sprintf(`
(function() {
@@ -238,18 +248,22 @@ func (d *Driver) Swipe(_ context.Context, fromX, fromY, toX, toY int, duration t
fromX, fromY,
toX, toY,
)
return chromedp.Run(d.tabCtx, chromedp.Evaluate(script, nil))
return chromedp.Run(runCtx, chromedp.Evaluate(script, nil))
}
func (d *Driver) PressKey(_ context.Context, key string) error {
func (d *Driver) PressKey(ctx context.Context, key string) error {
k, ok := keyMap[key]
if !ok {
return fmt.Errorf("unsupported key: %q", key)
}
return chromedp.Run(d.tabCtx, chromedp.KeyEvent(k))
runCtx, cancel := d.runCtx(ctx)
defer cancel()
return chromedp.Run(runCtx, chromedp.KeyEvent(k))
}
func (d *Driver) LongPress(_ context.Context, x, y int) error {
func (d *Driver) LongPress(ctx context.Context, x, y int) error {
runCtx, cancel := d.runCtx(ctx)
defer cancel()
script := fmt.Sprintf(`
(function() {
const el = document.elementFromPoint(%d, %d);
@@ -263,7 +277,7 @@ func (d *Driver) LongPress(_ context.Context, x, y int) error {
x, y,
x, y,
)
return chromedp.Run(d.tabCtx, chromedp.Evaluate(script, nil))
return chromedp.Run(runCtx, chromedp.Evaluate(script, nil))
}
// keyMap covers the keys web specs may emit (enter/tab/escape/arrows).
+42
View File
@@ -5,10 +5,52 @@ package chrome
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
)
// TestActionMethods_HonorCallerCancellation confirms the DeviceDriver action
// methods route through runCtx so a cancelled caller context aborts the CDP
// round-trip instead of blocking on d.tabCtx. Without this a hung browser would
// ignore step deadlines and Ctrl-C.
func TestActionMethods_HonorCallerCancellation(t *testing.T) {
d := New()
defer d.Terminate(context.Background())
launchCtx, launchCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer launchCancel()
if err := d.Launch(launchCtx, "data:text/html,<body><button id=go>go</button></body>", false, nil); err != nil {
t.Fatalf("Launch: %v", err)
}
cancelled, cancel := context.WithCancel(context.Background())
cancel()
actions := map[string]func() error{
"Tap": func() error { return d.Tap(cancelled, 1, 1) },
"Swipe": func() error { return d.Swipe(cancelled, 1, 1, 2, 2, 50*time.Millisecond) },
"LongPress": func() error { return d.LongPress(cancelled, 1, 1) },
"PressKey": func() error { return d.PressKey(cancelled, "enter") },
"InputText": func() error { return d.InputText(cancelled, "x") },
"EraseText": func() error { return d.EraseText(cancelled, 1) },
"TapSelector": func() error { return d.TapSelector(cancelled, "#go") },
}
for name, action := range actions {
t.Run(name, func(t *testing.T) {
done := make(chan error, 1)
go func() { done <- action() }()
select {
case err := <-done:
if !errors.Is(err, context.Canceled) {
t.Fatalf("want context.Canceled, got %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("action ignored cancelled caller ctx and blocked")
}
})
}
}
// TestHierarchy_EditableFlag confirms the injected hierarchy script marks text
// inputs, textareas, and contenteditable elements editable while leaving
// buttons and non-text inputs alone.