mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(ios): refuse a gesture the screen has no surface under
the hierarchy reaches past the screen wherever a scroll container holds content below the fold, so an action derived from it can name a point no touch lands on. tap, double tap, long press and swipe now report ErrGestureUndelivered for such a point, the far edge exclusive because a touch at x == screenWidth arrives at screenWidth-1. resolveSelectorCenter reports ErrSelectorMatchedNothing rather than a bare error.
This commit is contained in:
1 parent
da3cc5f68c
commit
0ab5c305e3
2 files changed
+141
-1
No files matched your search
@@ -594,7 +594,19 @@ func (d *Driver) Terminate(ctx context.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// offScreen reports a point the device has no surface under. The hierarchy
|
||||
// reaches past the screen wherever a scroll container holds content beyond the
|
||||
// fold, so an action derived from it can name a point no touch can land on.
|
||||
// The far edge is exclusive: a touch at x == screenWidth arrives at
|
||||
// screenWidth-1, which is a point the action never named.
|
||||
func (d *Driver) offScreen(x, y int) bool {
|
||||
return x < 0 || y < 0 || x >= d.screenWidth || y >= d.screenHeight
|
||||
}
|
||||
|
||||
func (d *Driver) Tap(ctx context.Context, x, y int) error {
|
||||
if d.offScreen(x, y) {
|
||||
return fmt.Errorf("%w: (%d,%d)", driver.ErrGestureUndelivered, x, y)
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.lastTap.x = float64(x)
|
||||
d.lastTap.y = float64(y)
|
||||
@@ -606,18 +618,27 @@ func (d *Driver) Tap(ctx context.Context, x, y int) error {
|
||||
}
|
||||
|
||||
func (d *Driver) DoubleTap(ctx context.Context, x, y int) error {
|
||||
if d.offScreen(x, y) {
|
||||
return fmt.Errorf("%w: (%d,%d)", driver.ErrGestureUndelivered, x, y)
|
||||
}
|
||||
return d.withRecovery(ctx, func() error {
|
||||
return d.companion.SendHID(ctx, doubleTapEvents(float64(x), float64(y), d.doubleTapGapMilliseconds)...)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Driver) LongPress(ctx context.Context, x, y int) error {
|
||||
if d.offScreen(x, y) {
|
||||
return fmt.Errorf("%w: (%d,%d)", driver.ErrGestureUndelivered, x, y)
|
||||
}
|
||||
return d.withRecovery(ctx, func() error {
|
||||
return d.companion.SendHID(ctx, longPressEvents(float64(x), float64(y), longPressHoldMilliseconds)...)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Driver) Swipe(ctx context.Context, fromX, fromY, toX, toY int, duration time.Duration) error {
|
||||
if d.offScreen(fromX, fromY) {
|
||||
return fmt.Errorf("%w: (%d,%d)", driver.ErrGestureUndelivered, fromX, fromY)
|
||||
}
|
||||
seconds := duration.Seconds()
|
||||
if seconds <= 0 {
|
||||
seconds = 0.25
|
||||
@@ -707,7 +728,7 @@ func (d *Driver) resolveSelectorCenter(ctx context.Context, selector string) (in
|
||||
}
|
||||
element := tree.Find(selector)
|
||||
if element == nil {
|
||||
return 0, 0, fmt.Errorf("selector %q matched no element", selector)
|
||||
return 0, 0, fmt.Errorf("%w: %q", driver.ErrSelectorMatchedNothing, selector)
|
||||
}
|
||||
x, y := element.Bounds.Center()
|
||||
return x, y, nil
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -1023,3 +1024,121 @@ func TestAcquireDeviceLockKeepsDistinctDevicesIndependent(t *testing.T) {
|
||||
}
|
||||
defer second.Close()
|
||||
}
|
||||
|
||||
func TestSwipeReportsAGestureThatStartsOffScreen(t *testing.T) {
|
||||
companion := &fakeCompanion{accessibilityJSON: "[]"}
|
||||
d := newTestDriver(companion)
|
||||
|
||||
// The hierarchy extends past the screen whenever a scroll container holds
|
||||
// content below the fold, so the runner's clamp to the tree's extent can
|
||||
// place a gesture where the device has no surface to receive it.
|
||||
err := d.Swipe(
|
||||
context.Background(),
|
||||
200,
|
||||
1200,
|
||||
200,
|
||||
800,
|
||||
300*time.Millisecond,
|
||||
)
|
||||
if !errors.Is(err, driver.ErrGestureUndelivered) {
|
||||
t.Fatalf(
|
||||
"Swipe starting below the screen: err = %v, want ErrGestureUndelivered",
|
||||
err,
|
||||
)
|
||||
}
|
||||
if slices.Contains(companion.recorded(), "hid") {
|
||||
t.Fatal("an undeliverable swipe must not reach the companion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwipeOnScreenReachesTheCompanion(t *testing.T) {
|
||||
companion := &fakeCompanion{accessibilityJSON: "[]"}
|
||||
d := newTestDriver(companion)
|
||||
|
||||
if err := d.Swipe(context.Background(), 200, 600, 200, 200, 300*time.Millisecond); err != nil {
|
||||
t.Fatalf("Swipe: %v", err)
|
||||
}
|
||||
if !slices.Contains(companion.recorded(), "hid") {
|
||||
t.Fatal("an on-screen swipe should reach the companion")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTapSelectorReportsASelectorThatMatchesNothing keeps a by-selector tap
|
||||
// that resolved to no element out of the steps that read as dispatched.
|
||||
func TestTapSelectorReportsASelectorThatMatchesNothing(t *testing.T) {
|
||||
companion := &fakeCompanion{accessibilityJSON: "[]"}
|
||||
d := newTestDriver(companion)
|
||||
|
||||
err := d.TapSelector(context.Background(), "id:absent")
|
||||
if !errors.Is(err, driver.ErrSelectorMatchedNothing) {
|
||||
t.Fatalf("TapSelector on an absent element: err = %v, want ErrSelectorMatchedNothing", err)
|
||||
}
|
||||
if slices.Contains(companion.recorded(), "hid") {
|
||||
t.Fatal("a selector that matched nothing must not reach the companion")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGesturesRefuseTheFarEdgeOfTheScreen holds iOS to the extent the device
|
||||
// enforces. A tap at x == screenWidth is not delivered at that point on the
|
||||
// simulator, so admitting it dispatches a gesture the app receives somewhere
|
||||
// other than where the trace says it landed.
|
||||
func TestGesturesRefuseTheFarEdgeOfTheScreen(t *testing.T) {
|
||||
gestures := map[string]func(*Driver, int, int) error{
|
||||
"Tap": func(d *Driver, x, y int) error { return d.Tap(context.Background(), x, y) },
|
||||
"DoubleTap": func(d *Driver, x, y int) error { return d.DoubleTap(context.Background(), x, y) },
|
||||
"LongPress": func(d *Driver, x, y int) error { return d.LongPress(context.Background(), x, y) },
|
||||
"Swipe": func(d *Driver, x, y int) error {
|
||||
return d.Swipe(context.Background(), x, y, 200, 400, 300*time.Millisecond)
|
||||
},
|
||||
}
|
||||
for name, gesture := range gestures {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for _, point := range []struct{ x, y int }{{390, 400}, {200, 844}} {
|
||||
companion := &fakeCompanion{accessibilityJSON: "[]"}
|
||||
d := newTestDriver(companion)
|
||||
err := gesture(d, point.x, point.y)
|
||||
if !errors.Is(err, driver.ErrGestureUndelivered) {
|
||||
t.Fatalf("%s at (%d,%d): err = %v, want ErrGestureUndelivered",
|
||||
name, point.x, point.y, err)
|
||||
}
|
||||
if slices.Contains(companion.recorded(), "hid") {
|
||||
t.Fatalf("%s at (%d,%d) reached the companion", name, point.x, point.y)
|
||||
}
|
||||
}
|
||||
companion := &fakeCompanion{accessibilityJSON: "[]"}
|
||||
d := newTestDriver(companion)
|
||||
if err := gesture(d, 389, 843); err != nil {
|
||||
t.Fatalf("%s at the last on-screen point: %v", name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTapReportsAPointOffScreen is the iOS half of letting the runner hand off
|
||||
// every point it resolved. The runner no longer refuses a point outside the
|
||||
// screen, because a driver that can scroll reaches it; this one cannot, and a
|
||||
// point with no surface under it has to be reported rather than synthesised
|
||||
// into nothing.
|
||||
func TestTapReportsAPointOffScreen(t *testing.T) {
|
||||
for _, point := range []struct {
|
||||
name string
|
||||
x, y int
|
||||
}{
|
||||
{"above the screen", 200, -208},
|
||||
{"below the screen", 200, 1200},
|
||||
} {
|
||||
t.Run(point.name, func(t *testing.T) {
|
||||
companion := &fakeCompanion{accessibilityJSON: "[]"}
|
||||
d := newTestDriver(companion)
|
||||
|
||||
err := d.Tap(context.Background(), point.x, point.y)
|
||||
if !errors.Is(err, driver.ErrGestureUndelivered) {
|
||||
t.Fatalf("Tap at (%d,%d): err = %v, want ErrGestureUndelivered",
|
||||
point.x, point.y, err)
|
||||
}
|
||||
if slices.Contains(companion.recorded(), "hid") {
|
||||
t.Fatal("an undeliverable tap must not reach the companion")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user