mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(runner): a swipe whose endpoints coincide is not dispatched
The scroll branch already refuses a zero-distance gesture, but Swipe clamped and dispatched unconditionally, so a collapsed swipe reached the driver as a 250ms press and hold the app reads as a tap and the step recorded an executed swipe. The guard reads the endpoints after clampGestureToSafeArea, because the clamp is one of the two ways a swipe collapses.
This commit is contained in:
1 parent
047076f9c3
commit
83d988afda
2 files changed
+166
No files matched your search
@@ -0,0 +1,152 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
||||||
|
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
||||||
|
"github.com/priyanshujain/sanderling/internal/verifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
// legacyAuthoredScrollWire is what @sanderling/spec 0.0.3 put on the wire for
|
||||||
|
// `Scroll({ in: container, direction: "down" })`: the container's own point as
|
||||||
|
// both endpoints. This binary reads pre-computed endpoints as authoritative,
|
||||||
|
// so the pair dispatches a 250ms press and hold that the app reads as a tap,
|
||||||
|
// with `direction` riding along unread.
|
||||||
|
const legacyAuthoredScrollWire = `{"kind":"Scroll","direction":"down",` +
|
||||||
|
`"fromX":540,"fromY":1200,"toX":540,"toY":1200,"durationMillis":250,` +
|
||||||
|
`"selector":"id:ledger"}`
|
||||||
|
|
||||||
|
func TestApplyAction_ZeroDistanceScrollNeverReachesTheDriver(t *testing.T) {
|
||||||
|
action, err := verifier.DecodeAction(json.RawMessage(legacyAuthoredScrollWire))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecodeAction: %v", err)
|
||||||
|
}
|
||||||
|
treeJSON := `{"attributes":{"bounds":"[0,0,1080,2400]"},"children":[
|
||||||
|
{"attributes":{"resource-id":"com.fixture:id/ledger","scrollable":"true","bounds":"[0,400,1080,2000]"},"children":[],"enabled":true}
|
||||||
|
]}`
|
||||||
|
tree, err := hierarchy.Parse(treeJSON)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse tree: %v", err)
|
||||||
|
}
|
||||||
|
driverMock := mockdriver.New()
|
||||||
|
|
||||||
|
skipped, err := applyAction(context.Background(), driverMock, action, tree)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("applyAction: %v", err)
|
||||||
|
}
|
||||||
|
for _, dispatched := range driverMock.Actions() {
|
||||||
|
if dispatched.Kind == mockdriver.ActionSwipe {
|
||||||
|
t.Fatalf("a scroll travelling zero distance reached the driver as %v; "+
|
||||||
|
"it is a press and hold, not a scroll", dispatched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if skipped != actionSkippedZeroDistanceScroll {
|
||||||
|
t.Errorf("applyAction reported %q, want %q: a step that scrolled nothing "+
|
||||||
|
"has to say so", skipped, actionSkippedZeroDistanceScroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyAction_ScrollWithRealDistanceStillReachesTheDriver(t *testing.T) {
|
||||||
|
driverMock := mockdriver.New()
|
||||||
|
action := verifier.Action{
|
||||||
|
Kind: verifier.ActionKindScroll,
|
||||||
|
Direction: "down",
|
||||||
|
FromX: 540,
|
||||||
|
FromY: 1200,
|
||||||
|
ToX: 540,
|
||||||
|
ToY: 600,
|
||||||
|
DurationMillis: 250,
|
||||||
|
}
|
||||||
|
|
||||||
|
mustDispatch(t, driverMock, action, nil)
|
||||||
|
found := false
|
||||||
|
for _, dispatched := range driverMock.Actions() {
|
||||||
|
if dispatched.Kind == mockdriver.ActionSwipe && dispatched.ToY == 600 {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("the scroll never reached the driver: %v", driverMock.Actions())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyAction_ZeroDistanceSwipeNeverReachesTheDriver(t *testing.T) {
|
||||||
|
driverMock := mockdriver.New()
|
||||||
|
action := verifier.Action{
|
||||||
|
Kind: verifier.ActionKindSwipe,
|
||||||
|
FromX: 540,
|
||||||
|
FromY: 1200,
|
||||||
|
ToX: 540,
|
||||||
|
ToY: 1200,
|
||||||
|
DurationMillis: 250,
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped, err := applyAction(context.Background(), driverMock, action, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("applyAction: %v", err)
|
||||||
|
}
|
||||||
|
if len(driverMock.Actions()) != 0 {
|
||||||
|
t.Fatalf("a swipe travelling zero distance reached the driver as %v; "+
|
||||||
|
"it is a press and hold, not a swipe", driverMock.Actions())
|
||||||
|
}
|
||||||
|
if skipped != actionSkippedZeroDistanceSwipe {
|
||||||
|
t.Errorf("applyAction reported %q, want %q", skipped, actionSkippedZeroDistanceSwipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clampGestureToSafeArea can collapse a swipe that was authored with real
|
||||||
|
// distance, so the guard has to read the endpoints it is about to dispatch
|
||||||
|
// rather than the ones the action carried.
|
||||||
|
func TestApplyAction_SwipeCollapsedByTheSafeAreaClampNeverReachesTheDriver(t *testing.T) {
|
||||||
|
tree, err := hierarchy.Parse(`{"attributes":{"bounds":"[0,0,1080,2400]"},"children":[]}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse tree: %v", err)
|
||||||
|
}
|
||||||
|
driverMock := mockdriver.New()
|
||||||
|
action := verifier.Action{
|
||||||
|
Kind: verifier.ActionKindSwipe,
|
||||||
|
FromX: 540,
|
||||||
|
FromY: 2500,
|
||||||
|
ToX: 540,
|
||||||
|
ToY: 2600,
|
||||||
|
DurationMillis: 250,
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped, err := applyAction(context.Background(), driverMock, action, tree)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("applyAction: %v", err)
|
||||||
|
}
|
||||||
|
if len(driverMock.Actions()) != 0 {
|
||||||
|
t.Fatalf("a swipe the clamp collapsed onto one point reached the driver as %v",
|
||||||
|
driverMock.Actions())
|
||||||
|
}
|
||||||
|
if skipped != actionSkippedZeroDistanceSwipe {
|
||||||
|
t.Errorf("applyAction reported %q, want %q", skipped, actionSkippedZeroDistanceSwipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyAction_SwipeWithRealDistanceStillReachesTheDriver(t *testing.T) {
|
||||||
|
driverMock := mockdriver.New()
|
||||||
|
action := verifier.Action{
|
||||||
|
Kind: verifier.ActionKindSwipe,
|
||||||
|
FromX: 540,
|
||||||
|
FromY: 1800,
|
||||||
|
ToX: 540,
|
||||||
|
ToY: 600,
|
||||||
|
DurationMillis: 250,
|
||||||
|
}
|
||||||
|
|
||||||
|
mustDispatch(t, driverMock, action, nil)
|
||||||
|
found := false
|
||||||
|
for _, dispatched := range driverMock.Actions() {
|
||||||
|
if dispatched.Kind == mockdriver.ActionSwipe && dispatched.ToY == 600 {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("the swipe never reached the driver: %v", driverMock.Actions())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -975,6 +975,9 @@ func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.A
|
|||||||
case verifier.ActionKindScroll:
|
case verifier.ActionKindScroll:
|
||||||
fromX, fromY, toX, toY := scrollEndpoints(action, tree)
|
fromX, fromY, toX, toY := scrollEndpoints(action, tree)
|
||||||
fromX, fromY, toX, toY = clampGestureToSafeArea(fromX, fromY, toX, toY, screenBounds(tree))
|
fromX, fromY, toX, toY = clampGestureToSafeArea(fromX, fromY, toX, toY, screenBounds(tree))
|
||||||
|
if fromX == toX && fromY == toY {
|
||||||
|
return actionSkippedZeroDistanceScroll, nil
|
||||||
|
}
|
||||||
duration := time.Duration(action.DurationMillis) * time.Millisecond
|
duration := time.Duration(action.DurationMillis) * time.Millisecond
|
||||||
if duration <= 0 {
|
if duration <= 0 {
|
||||||
duration = 300 * time.Millisecond
|
duration = 300 * time.Millisecond
|
||||||
@@ -1032,6 +1035,9 @@ func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.A
|
|||||||
duration = 250 * time.Millisecond
|
duration = 250 * time.Millisecond
|
||||||
}
|
}
|
||||||
fromX, fromY, toX, toY := clampGestureToSafeArea(action.FromX, action.FromY, action.ToX, action.ToY, screenBounds(tree))
|
fromX, fromY, toX, toY := clampGestureToSafeArea(action.FromX, action.FromY, action.ToX, action.ToY, screenBounds(tree))
|
||||||
|
if fromX == toX && fromY == toY {
|
||||||
|
return actionSkippedZeroDistanceSwipe, nil
|
||||||
|
}
|
||||||
return "", drv.Swipe(ctx, fromX, fromY, toX, toY, duration)
|
return "", drv.Swipe(ctx, fromX, fromY, toX, toY, duration)
|
||||||
case verifier.ActionKindPressKey:
|
case verifier.ActionKindPressKey:
|
||||||
if action.Key == "" {
|
if action.Key == "" {
|
||||||
@@ -1766,6 +1772,14 @@ const (
|
|||||||
actionSkippedUnresolvedSelector actionSkipReason = "unresolved_selector"
|
actionSkippedUnresolvedSelector actionSkipReason = "unresolved_selector"
|
||||||
actionSkippedMissingKey actionSkipReason = "missing_key"
|
actionSkippedMissingKey actionSkipReason = "missing_key"
|
||||||
actionSkippedZeroDurationWait actionSkipReason = "zero_duration_wait"
|
actionSkippedZeroDurationWait actionSkipReason = "zero_duration_wait"
|
||||||
|
// A gesture whose endpoints coincide is a press and hold the app reads as
|
||||||
|
// a tap, not a drag that moved nothing: dispatching it records an executed
|
||||||
|
// scroll or swipe for a gesture that could never travel. The two are
|
||||||
|
// reported apart because they arrive from different places, a scroll from
|
||||||
|
// a container whose own point became both endpoints and a swipe from
|
||||||
|
// authored coordinates or a clamp that collapsed them.
|
||||||
|
actionSkippedZeroDistanceScroll actionSkipReason = "zero_distance_scroll"
|
||||||
|
actionSkippedZeroDistanceSwipe actionSkipReason = "zero_distance_swipe"
|
||||||
// The driver resolved the action's point and found no element there, so
|
// The driver resolved the action's point and found no element there, so
|
||||||
// the gesture was never dispatched. Recorded rather than counted as a
|
// the gesture was never dispatched. Recorded rather than counted as a
|
||||||
// device fault: a run that acts on nothing has to say so.
|
// device fault: a run that acts on nothing has to say so.
|
||||||
|
|||||||
Reference in new issue
Block a user