mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(driver): native DoubleTap RPC for a tight inter-tap gap
Composing two Tap round trips from the Go client spread the taps by hundreds of milliseconds on iOS, wide enough for the app to navigate between them, so double-submission races could never reproduce. The sidecar now lands both taps back-to-back next to the device transport.
This commit is contained in:
1 parent
d6188df667
commit
4f579d2a0b
8 files changed
+151
-38
No files matched your search
@@ -104,14 +104,18 @@ func (c *Client) TapSelector(ctx context.Context, selector string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// doubleTapGap is the inter-tap delay for DoubleTap: short enough to land both
|
// doubleTapGap is the inter-tap delay for the selector fallback: short enough
|
||||||
// events inside a sub-100 ms race window, long enough for the sidecar to
|
// to land both events inside a sub-100 ms race window, long enough for the
|
||||||
// serialize two MotionEvent streams. The sidecar exposes no native double-tap
|
// sidecar to serialize two MotionEvent streams.
|
||||||
// RPC, so the gesture is two Taps with this gap.
|
|
||||||
const doubleTapGap = 50 * time.Millisecond
|
const doubleTapGap = 50 * time.Millisecond
|
||||||
|
|
||||||
|
// DoubleTap dispatches the native RPC so the backend can land both taps as
|
||||||
|
// close together as the platform allows. Composing two Tap round trips from
|
||||||
|
// here spreads them by hundreds of milliseconds on iOS, wide enough for
|
||||||
|
// navigation to interleave between the taps.
|
||||||
func (c *Client) DoubleTap(ctx context.Context, x, y int) error {
|
func (c *Client) DoubleTap(ctx context.Context, x, y int) error {
|
||||||
return doubleTap(ctx, func() error { return c.Tap(ctx, x, y) })
|
_, err := c.stub.DoubleTap(ctx, &driverpb.Point{X: int32(x), Y: int32(y)})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DoubleTapSelector(ctx context.Context, selector string) error {
|
func (c *Client) DoubleTapSelector(ctx context.Context, selector string) error {
|
||||||
|
|||||||
+37
-33
@@ -1003,11 +1003,13 @@ const file_driverpb_driver_proto_rawDesc = "" +
|
|||||||
"cpuPercent\x12\x1d\n" +
|
"cpuPercent\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"heap_bytes\x18\x02 \x01(\x03R\theapBytes\x12,\n" +
|
"heap_bytes\x18\x02 \x01(\x03R\theapBytes\x12,\n" +
|
||||||
"\x12total_memory_bytes\x18\x03 \x01(\x03R\x10totalMemoryBytes2\xd1\t\n" +
|
"\x12total_memory_bytes\x18\x03 \x01(\x03R\x10totalMemoryBytes2\x98\n" +
|
||||||
|
"\n" +
|
||||||
"\x06Driver\x12J\n" +
|
"\x06Driver\x12J\n" +
|
||||||
"\x06Launch\x12#.sanderling.driver.v1.LaunchRequest\x1a\x1b.sanderling.driver.v1.Empty\x12E\n" +
|
"\x06Launch\x12#.sanderling.driver.v1.LaunchRequest\x1a\x1b.sanderling.driver.v1.Empty\x12E\n" +
|
||||||
"\tTerminate\x12\x1b.sanderling.driver.v1.Empty\x1a\x1b.sanderling.driver.v1.Empty\x12?\n" +
|
"\tTerminate\x12\x1b.sanderling.driver.v1.Empty\x1a\x1b.sanderling.driver.v1.Empty\x12?\n" +
|
||||||
"\x03Tap\x12\x1b.sanderling.driver.v1.Point\x1a\x1b.sanderling.driver.v1.Empty\x12J\n" +
|
"\x03Tap\x12\x1b.sanderling.driver.v1.Point\x1a\x1b.sanderling.driver.v1.Empty\x12E\n" +
|
||||||
|
"\tDoubleTap\x12\x1b.sanderling.driver.v1.Point\x1a\x1b.sanderling.driver.v1.Empty\x12J\n" +
|
||||||
"\vTapSelector\x12\x1e.sanderling.driver.v1.Selector\x1a\x1b.sanderling.driver.v1.Empty\x12D\n" +
|
"\vTapSelector\x12\x1e.sanderling.driver.v1.Selector\x1a\x1b.sanderling.driver.v1.Empty\x12D\n" +
|
||||||
"\tInputText\x12\x1a.sanderling.driver.v1.Text\x1a\x1b.sanderling.driver.v1.Empty\x12P\n" +
|
"\tInputText\x12\x1a.sanderling.driver.v1.Text\x1a\x1b.sanderling.driver.v1.Empty\x12P\n" +
|
||||||
"\tEraseText\x12&.sanderling.driver.v1.EraseTextRequest\x1a\x1b.sanderling.driver.v1.Empty\x12H\n" +
|
"\tEraseText\x12&.sanderling.driver.v1.EraseTextRequest\x1a\x1b.sanderling.driver.v1.Empty\x12H\n" +
|
||||||
@@ -1069,37 +1071,39 @@ var file_driverpb_driver_proto_depIdxs = []int32{
|
|||||||
1, // 6: sanderling.driver.v1.Driver.Launch:input_type -> sanderling.driver.v1.LaunchRequest
|
1, // 6: sanderling.driver.v1.Driver.Launch:input_type -> sanderling.driver.v1.LaunchRequest
|
||||||
0, // 7: sanderling.driver.v1.Driver.Terminate:input_type -> sanderling.driver.v1.Empty
|
0, // 7: sanderling.driver.v1.Driver.Terminate:input_type -> sanderling.driver.v1.Empty
|
||||||
2, // 8: sanderling.driver.v1.Driver.Tap:input_type -> sanderling.driver.v1.Point
|
2, // 8: sanderling.driver.v1.Driver.Tap:input_type -> sanderling.driver.v1.Point
|
||||||
5, // 9: sanderling.driver.v1.Driver.TapSelector:input_type -> sanderling.driver.v1.Selector
|
2, // 9: sanderling.driver.v1.Driver.DoubleTap:input_type -> sanderling.driver.v1.Point
|
||||||
3, // 10: sanderling.driver.v1.Driver.InputText:input_type -> sanderling.driver.v1.Text
|
5, // 10: sanderling.driver.v1.Driver.TapSelector:input_type -> sanderling.driver.v1.Selector
|
||||||
4, // 11: sanderling.driver.v1.Driver.EraseText:input_type -> sanderling.driver.v1.EraseTextRequest
|
3, // 11: sanderling.driver.v1.Driver.InputText:input_type -> sanderling.driver.v1.Text
|
||||||
11, // 12: sanderling.driver.v1.Driver.Swipe:input_type -> sanderling.driver.v1.SwipeRequest
|
4, // 12: sanderling.driver.v1.Driver.EraseText:input_type -> sanderling.driver.v1.EraseTextRequest
|
||||||
12, // 13: sanderling.driver.v1.Driver.PressKey:input_type -> sanderling.driver.v1.PressKeyRequest
|
11, // 13: sanderling.driver.v1.Driver.Swipe:input_type -> sanderling.driver.v1.SwipeRequest
|
||||||
2, // 14: sanderling.driver.v1.Driver.LongPress:input_type -> sanderling.driver.v1.Point
|
12, // 14: sanderling.driver.v1.Driver.PressKey:input_type -> sanderling.driver.v1.PressKeyRequest
|
||||||
0, // 15: sanderling.driver.v1.Driver.Screenshot:input_type -> sanderling.driver.v1.Empty
|
2, // 15: sanderling.driver.v1.Driver.LongPress:input_type -> sanderling.driver.v1.Point
|
||||||
0, // 16: sanderling.driver.v1.Driver.Hierarchy:input_type -> sanderling.driver.v1.Empty
|
0, // 16: sanderling.driver.v1.Driver.Screenshot:input_type -> sanderling.driver.v1.Empty
|
||||||
0, // 17: sanderling.driver.v1.Driver.Snapshot:input_type -> sanderling.driver.v1.Empty
|
0, // 17: sanderling.driver.v1.Driver.Hierarchy:input_type -> sanderling.driver.v1.Empty
|
||||||
13, // 18: sanderling.driver.v1.Driver.RecentLogs:input_type -> sanderling.driver.v1.RecentLogsRequest
|
0, // 18: sanderling.driver.v1.Driver.Snapshot:input_type -> sanderling.driver.v1.Empty
|
||||||
9, // 19: sanderling.driver.v1.Driver.WaitForIdle:input_type -> sanderling.driver.v1.Duration
|
13, // 19: sanderling.driver.v1.Driver.RecentLogs:input_type -> sanderling.driver.v1.RecentLogsRequest
|
||||||
0, // 20: sanderling.driver.v1.Driver.Health:input_type -> sanderling.driver.v1.Empty
|
9, // 20: sanderling.driver.v1.Driver.WaitForIdle:input_type -> sanderling.driver.v1.Duration
|
||||||
16, // 21: sanderling.driver.v1.Driver.Metrics:input_type -> sanderling.driver.v1.MetricsRequest
|
0, // 21: sanderling.driver.v1.Driver.Health:input_type -> sanderling.driver.v1.Empty
|
||||||
0, // 22: sanderling.driver.v1.Driver.Launch:output_type -> sanderling.driver.v1.Empty
|
16, // 22: sanderling.driver.v1.Driver.Metrics:input_type -> sanderling.driver.v1.MetricsRequest
|
||||||
0, // 23: sanderling.driver.v1.Driver.Terminate:output_type -> sanderling.driver.v1.Empty
|
0, // 23: sanderling.driver.v1.Driver.Launch:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 24: sanderling.driver.v1.Driver.Tap:output_type -> sanderling.driver.v1.Empty
|
0, // 24: sanderling.driver.v1.Driver.Terminate:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 25: sanderling.driver.v1.Driver.TapSelector:output_type -> sanderling.driver.v1.Empty
|
0, // 25: sanderling.driver.v1.Driver.Tap:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 26: sanderling.driver.v1.Driver.InputText:output_type -> sanderling.driver.v1.Empty
|
0, // 26: sanderling.driver.v1.Driver.DoubleTap:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 27: sanderling.driver.v1.Driver.EraseText:output_type -> sanderling.driver.v1.Empty
|
0, // 27: sanderling.driver.v1.Driver.TapSelector:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 28: sanderling.driver.v1.Driver.Swipe:output_type -> sanderling.driver.v1.Empty
|
0, // 28: sanderling.driver.v1.Driver.InputText:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 29: sanderling.driver.v1.Driver.PressKey:output_type -> sanderling.driver.v1.Empty
|
0, // 29: sanderling.driver.v1.Driver.EraseText:output_type -> sanderling.driver.v1.Empty
|
||||||
0, // 30: sanderling.driver.v1.Driver.LongPress:output_type -> sanderling.driver.v1.Empty
|
0, // 30: sanderling.driver.v1.Driver.Swipe:output_type -> sanderling.driver.v1.Empty
|
||||||
6, // 31: sanderling.driver.v1.Driver.Screenshot:output_type -> sanderling.driver.v1.Image
|
0, // 31: sanderling.driver.v1.Driver.PressKey:output_type -> sanderling.driver.v1.Empty
|
||||||
7, // 32: sanderling.driver.v1.Driver.Hierarchy:output_type -> sanderling.driver.v1.HierarchyJSON
|
0, // 32: sanderling.driver.v1.Driver.LongPress:output_type -> sanderling.driver.v1.Empty
|
||||||
8, // 33: sanderling.driver.v1.Driver.Snapshot:output_type -> sanderling.driver.v1.SnapshotResponse
|
6, // 33: sanderling.driver.v1.Driver.Screenshot:output_type -> sanderling.driver.v1.Image
|
||||||
14, // 34: sanderling.driver.v1.Driver.RecentLogs:output_type -> sanderling.driver.v1.LogEntries
|
7, // 34: sanderling.driver.v1.Driver.Hierarchy:output_type -> sanderling.driver.v1.HierarchyJSON
|
||||||
0, // 35: sanderling.driver.v1.Driver.WaitForIdle:output_type -> sanderling.driver.v1.Empty
|
8, // 35: sanderling.driver.v1.Driver.Snapshot:output_type -> sanderling.driver.v1.SnapshotResponse
|
||||||
10, // 36: sanderling.driver.v1.Driver.Health:output_type -> sanderling.driver.v1.HealthStatus
|
14, // 36: sanderling.driver.v1.Driver.RecentLogs:output_type -> sanderling.driver.v1.LogEntries
|
||||||
17, // 37: sanderling.driver.v1.Driver.Metrics:output_type -> sanderling.driver.v1.MetricsResponse
|
0, // 37: sanderling.driver.v1.Driver.WaitForIdle:output_type -> sanderling.driver.v1.Empty
|
||||||
22, // [22:38] is the sub-list for method output_type
|
10, // 38: sanderling.driver.v1.Driver.Health:output_type -> sanderling.driver.v1.HealthStatus
|
||||||
6, // [6:22] is the sub-list for method input_type
|
17, // 39: sanderling.driver.v1.Driver.Metrics:output_type -> sanderling.driver.v1.MetricsResponse
|
||||||
|
23, // [23:40] is the sub-list for method output_type
|
||||||
|
6, // [6:23] is the sub-list for method input_type
|
||||||
6, // [6:6] is the sub-list for extension type_name
|
6, // [6:6] is the sub-list for extension type_name
|
||||||
6, // [6:6] is the sub-list for extension extendee
|
6, // [6:6] is the sub-list for extension extendee
|
||||||
0, // [0:6] is the sub-list for field type_name
|
0, // [0:6] is the sub-list for field type_name
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ service Driver {
|
|||||||
rpc Terminate(Empty) returns (Empty);
|
rpc Terminate(Empty) returns (Empty);
|
||||||
|
|
||||||
rpc Tap(Point) returns (Empty);
|
rpc Tap(Point) returns (Empty);
|
||||||
|
// DoubleTap lands two taps as close together as the platform allows.
|
||||||
|
// Composing two Tap calls from the client spreads them by hundreds of
|
||||||
|
// milliseconds on iOS, wide enough for navigation to interleave, which
|
||||||
|
// defeats double-submission testing.
|
||||||
|
rpc DoubleTap(Point) returns (Empty);
|
||||||
rpc TapSelector(Selector) returns (Empty);
|
rpc TapSelector(Selector) returns (Empty);
|
||||||
rpc InputText(Text) returns (Empty);
|
rpc InputText(Text) returns (Empty);
|
||||||
// EraseText deletes characters from the focused field so InputText can
|
// EraseText deletes characters from the focused field so InputText can
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const (
|
|||||||
Driver_Launch_FullMethodName = "/sanderling.driver.v1.Driver/Launch"
|
Driver_Launch_FullMethodName = "/sanderling.driver.v1.Driver/Launch"
|
||||||
Driver_Terminate_FullMethodName = "/sanderling.driver.v1.Driver/Terminate"
|
Driver_Terminate_FullMethodName = "/sanderling.driver.v1.Driver/Terminate"
|
||||||
Driver_Tap_FullMethodName = "/sanderling.driver.v1.Driver/Tap"
|
Driver_Tap_FullMethodName = "/sanderling.driver.v1.Driver/Tap"
|
||||||
|
Driver_DoubleTap_FullMethodName = "/sanderling.driver.v1.Driver/DoubleTap"
|
||||||
Driver_TapSelector_FullMethodName = "/sanderling.driver.v1.Driver/TapSelector"
|
Driver_TapSelector_FullMethodName = "/sanderling.driver.v1.Driver/TapSelector"
|
||||||
Driver_InputText_FullMethodName = "/sanderling.driver.v1.Driver/InputText"
|
Driver_InputText_FullMethodName = "/sanderling.driver.v1.Driver/InputText"
|
||||||
Driver_EraseText_FullMethodName = "/sanderling.driver.v1.Driver/EraseText"
|
Driver_EraseText_FullMethodName = "/sanderling.driver.v1.Driver/EraseText"
|
||||||
@@ -44,6 +45,11 @@ type DriverClient interface {
|
|||||||
Launch(ctx context.Context, in *LaunchRequest, opts ...grpc.CallOption) (*Empty, error)
|
Launch(ctx context.Context, in *LaunchRequest, opts ...grpc.CallOption) (*Empty, error)
|
||||||
Terminate(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error)
|
Terminate(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error)
|
||||||
Tap(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Empty, error)
|
Tap(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Empty, error)
|
||||||
|
// DoubleTap lands two taps as close together as the platform allows.
|
||||||
|
// Composing two Tap calls from the client spreads them by hundreds of
|
||||||
|
// milliseconds on iOS, wide enough for navigation to interleave, which
|
||||||
|
// defeats double-submission testing.
|
||||||
|
DoubleTap(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Empty, error)
|
||||||
TapSelector(ctx context.Context, in *Selector, opts ...grpc.CallOption) (*Empty, error)
|
TapSelector(ctx context.Context, in *Selector, opts ...grpc.CallOption) (*Empty, error)
|
||||||
InputText(ctx context.Context, in *Text, opts ...grpc.CallOption) (*Empty, error)
|
InputText(ctx context.Context, in *Text, opts ...grpc.CallOption) (*Empty, error)
|
||||||
// EraseText deletes characters from the focused field so InputText can
|
// EraseText deletes characters from the focused field so InputText can
|
||||||
@@ -102,6 +108,16 @@ func (c *driverClient) Tap(ctx context.Context, in *Point, opts ...grpc.CallOpti
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *driverClient) DoubleTap(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Empty, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Driver_DoubleTap_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *driverClient) TapSelector(ctx context.Context, in *Selector, opts ...grpc.CallOption) (*Empty, error) {
|
func (c *driverClient) TapSelector(ctx context.Context, in *Selector, opts ...grpc.CallOption) (*Empty, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(Empty)
|
out := new(Empty)
|
||||||
@@ -239,6 +255,11 @@ type DriverServer interface {
|
|||||||
Launch(context.Context, *LaunchRequest) (*Empty, error)
|
Launch(context.Context, *LaunchRequest) (*Empty, error)
|
||||||
Terminate(context.Context, *Empty) (*Empty, error)
|
Terminate(context.Context, *Empty) (*Empty, error)
|
||||||
Tap(context.Context, *Point) (*Empty, error)
|
Tap(context.Context, *Point) (*Empty, error)
|
||||||
|
// DoubleTap lands two taps as close together as the platform allows.
|
||||||
|
// Composing two Tap calls from the client spreads them by hundreds of
|
||||||
|
// milliseconds on iOS, wide enough for navigation to interleave, which
|
||||||
|
// defeats double-submission testing.
|
||||||
|
DoubleTap(context.Context, *Point) (*Empty, error)
|
||||||
TapSelector(context.Context, *Selector) (*Empty, error)
|
TapSelector(context.Context, *Selector) (*Empty, error)
|
||||||
InputText(context.Context, *Text) (*Empty, error)
|
InputText(context.Context, *Text) (*Empty, error)
|
||||||
// EraseText deletes characters from the focused field so InputText can
|
// EraseText deletes characters from the focused field so InputText can
|
||||||
@@ -276,6 +297,9 @@ func (UnimplementedDriverServer) Terminate(context.Context, *Empty) (*Empty, err
|
|||||||
func (UnimplementedDriverServer) Tap(context.Context, *Point) (*Empty, error) {
|
func (UnimplementedDriverServer) Tap(context.Context, *Point) (*Empty, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method Tap not implemented")
|
return nil, status.Error(codes.Unimplemented, "method Tap not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedDriverServer) DoubleTap(context.Context, *Point) (*Empty, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method DoubleTap not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedDriverServer) TapSelector(context.Context, *Selector) (*Empty, error) {
|
func (UnimplementedDriverServer) TapSelector(context.Context, *Selector) (*Empty, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method TapSelector not implemented")
|
return nil, status.Error(codes.Unimplemented, "method TapSelector not implemented")
|
||||||
}
|
}
|
||||||
@@ -390,6 +414,24 @@ func _Driver_Tap_Handler(srv interface{}, ctx context.Context, dec func(interfac
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Driver_DoubleTap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(Point)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DriverServer).DoubleTap(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Driver_DoubleTap_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DriverServer).DoubleTap(ctx, req.(*Point))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _Driver_TapSelector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _Driver_TapSelector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(Selector)
|
in := new(Selector)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -643,6 +685,10 @@ var Driver_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Tap",
|
MethodName: "Tap",
|
||||||
Handler: _Driver_Tap_Handler,
|
Handler: _Driver_Tap_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DoubleTap",
|
||||||
|
Handler: _Driver_DoubleTap_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "TapSelector",
|
MethodName: "TapSelector",
|
||||||
Handler: _Driver_TapSelector_Handler,
|
Handler: _Driver_TapSelector_Handler,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ func TestDriverServiceDescriptor(t *testing.T) {
|
|||||||
"Launch": true,
|
"Launch": true,
|
||||||
"Terminate": true,
|
"Terminate": true,
|
||||||
"Tap": true,
|
"Tap": true,
|
||||||
|
"DoubleTap": true,
|
||||||
"TapSelector": true,
|
"TapSelector": true,
|
||||||
"InputText": true,
|
"InputText": true,
|
||||||
"EraseText": true,
|
"EraseText": true,
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ interface DriverBackend {
|
|||||||
fun launch(bundleId: String, clearState: Boolean, env: Map<String, String> = emptyMap())
|
fun launch(bundleId: String, clearState: Boolean, env: Map<String, String> = emptyMap())
|
||||||
fun terminate(bundleId: String)
|
fun terminate(bundleId: String)
|
||||||
fun tap(x: Int, y: Int)
|
fun tap(x: Int, y: Int)
|
||||||
|
|
||||||
|
// doubleTap lands two taps as close together as the platform allows.
|
||||||
|
// The default composes two taps back-to-back; backends with higher
|
||||||
|
// per-tap latency override to tighten the gap.
|
||||||
|
fun doubleTap(x: Int, y: Int) {
|
||||||
|
tap(x, y)
|
||||||
|
tap(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
fun tapSelector(selector: String)
|
fun tapSelector(selector: String)
|
||||||
fun inputText(text: String)
|
fun inputText(text: String)
|
||||||
fun eraseText(characterCount: Int)
|
fun eraseText(characterCount: Int)
|
||||||
@@ -735,6 +744,15 @@ class IosDriverBackend(private val udid: String) : DriverBackend {
|
|||||||
|
|
||||||
override fun tap(x: Int, y: Int) = withReconnect { driver.tap(maestro.Point(x, y)) }
|
override fun tap(x: Int, y: Int) = withReconnect { driver.tap(maestro.Point(x, y)) }
|
||||||
|
|
||||||
|
// Both taps inside one reconnect scope, with nothing between them: the
|
||||||
|
// XCTest transport adds hundreds of milliseconds per round trip, so any
|
||||||
|
// client-side composition spreads the taps wide enough for the app to
|
||||||
|
// navigate between them.
|
||||||
|
override fun doubleTap(x: Int, y: Int) = withReconnect {
|
||||||
|
driver.tap(maestro.Point(x, y))
|
||||||
|
driver.tap(maestro.Point(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
override fun longPress(x: Int, y: Int) = withReconnect { driver.longPress(maestro.Point(x, y)) }
|
override fun longPress(x: Int, y: Int) = withReconnect { driver.longPress(maestro.Point(x, y)) }
|
||||||
|
|
||||||
override fun tapSelector(selector: String) = withReconnect {
|
override fun tapSelector(selector: String) = withReconnect {
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ class DriverService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun doubleTap(request: Point, responseObserver: StreamObserver<Empty>) {
|
||||||
|
runRpc(responseObserver) {
|
||||||
|
backend.doubleTap(request.x, request.y)
|
||||||
|
Empty.getDefaultInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun longPress(request: Point, responseObserver: StreamObserver<Empty>) {
|
override fun longPress(request: Point, responseObserver: StreamObserver<Empty>) {
|
||||||
runRpc(responseObserver) {
|
runRpc(responseObserver) {
|
||||||
backend.longPress(request.x, request.y)
|
backend.longPress(request.x, request.y)
|
||||||
|
|||||||
@@ -83,6 +83,34 @@ class DriverServiceTest {
|
|||||||
assertEquals("hello world", backend.lastInputText)
|
assertEquals("hello world", backend.lastInputText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test fun doubleTapDefaultComposesTwoTaps() {
|
||||||
|
// Interface delegation would bind the default doubleTap to the
|
||||||
|
// delegate, bypassing the tap override, so implement the interface
|
||||||
|
// directly.
|
||||||
|
val taps = mutableListOf<Pair<Int, Int>>()
|
||||||
|
val backend = object : DriverBackend {
|
||||||
|
override fun launch(bundleId: String, clearState: Boolean, env: Map<String, String>) {}
|
||||||
|
override fun terminate(bundleId: String) {}
|
||||||
|
override fun tap(x: Int, y: Int) { taps.add(x to y) }
|
||||||
|
override fun tapSelector(selector: String) {}
|
||||||
|
override fun inputText(text: String) {}
|
||||||
|
override fun eraseText(characterCount: Int) {}
|
||||||
|
override fun swipe(fromX: Int, fromY: Int, toX: Int, toY: Int, durationMillis: Long) {}
|
||||||
|
override fun pressKey(key: String) {}
|
||||||
|
override fun longPress(x: Int, y: Int) {}
|
||||||
|
override fun screenshot(): Triple<ByteArray, Int, Int> = Triple(byteArrayOf(), 0, 0)
|
||||||
|
override fun hierarchy(): String = "{}"
|
||||||
|
override fun recentLogs(sinceUnixMillis: Long, minLevel: String): List<LogLine> = emptyList()
|
||||||
|
override fun waitForIdle(durationMillis: Long) {}
|
||||||
|
override fun healthy(): Boolean = true
|
||||||
|
override fun metrics(bundleId: String): MetricsSample = MetricsSample(0.0, 0L, 0L)
|
||||||
|
}
|
||||||
|
val client = newClient(backend)
|
||||||
|
|
||||||
|
client.doubleTap(Point.newBuilder().setX(120).setY(340).build())
|
||||||
|
assertEquals(listOf(120 to 340, 120 to 340), taps)
|
||||||
|
}
|
||||||
|
|
||||||
@Test fun eraseTextForwardsCharacterCount() {
|
@Test fun eraseTextForwardsCharacterCount() {
|
||||||
val backend = StubDriverBackend("android")
|
val backend = StubDriverBackend("android")
|
||||||
val client = newClient(backend)
|
val client = newClient(backend)
|
||||||
|
|||||||
Reference in new issue
Block a user