From 17f55b69baa664f5c6b49d3408cc43fd1b5da015 Mon Sep 17 00:00:00 2001 From: Matej Resetar Date: Tue, 9 Jan 2024 14:44:23 +0100 Subject: [PATCH 1/8] Implement native tap at coordinates method for Android --- dev/e2e_app/integration_test/tap_at_test.dart | 26 +++++++++++++++++++ .../kotlin/pl/leancode/patrol/Automator.kt | 26 +++++++++++++++++++ .../pl/leancode/patrol/AutomatorServer.kt | 8 ++++++ .../pl/leancode/patrol/contracts/Contracts.kt | 6 +++++ .../patrol/contracts/NativeAutomatorServer.kt | 6 +++++ .../lib/src/native/contracts/contracts.dart | 25 ++++++++++++++++++ .../lib/src/native/contracts/contracts.g.dart | 13 ++++++++++ .../contracts/native_automator_client.dart | 9 +++++++ .../lib/src/native/native_automator.dart | 18 +++++++++++++ 9 files changed, 137 insertions(+) create mode 100644 dev/e2e_app/integration_test/tap_at_test.dart diff --git a/dev/e2e_app/integration_test/tap_at_test.dart b/dev/e2e_app/integration_test/tap_at_test.dart new file mode 100644 index 000000000..0511fd081 --- /dev/null +++ b/dev/e2e_app/integration_test/tap_at_test.dart @@ -0,0 +1,26 @@ +import 'dart:io' show Platform; + +import 'common.dart'; + +void main() { + final String appId; + if (Platform.isIOS) { + appId = 'com.apple.Preferences'; + } else if (Platform.isAndroid) { + appId = 'com.android.settings'; + } else { + throw UnsupportedError('Unsupported platform'); + } + + patrol('taps at the middle of the screen in the Settings app', ($) async { + await createApp($); + + await $.native.openApp(appId: appId); + // Native view with the text "Bluetooth" is universally present in the settings app + await $.native.waitUntilVisible(Selector(textContains: 'Bluetooth')); + await $.native.tapAt( + Offset(0.5, 0.5), + appId: appId, + ); + }); +} diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt index 546d53a1a..8b06b5d52 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt @@ -187,6 +187,31 @@ class Automator private constructor() { delay() } + fun tapAt(x: Float, y: Float) { + Logger.d("tapAt(x: $x, y: $y)") + + if (x !in 0f..1f) { + throw IllegalArgumentException("x represents a percentage and must be between 0 and 1") + } + + if (y !in 0f..1f) { + throw IllegalArgumentException("y represents a percentage and must be between 0 and 1") + } + + val displayX = (uiDevice.displayWidth * x).roundToInt() + val displayY = (uiDevice.displayHeight * y).roundToInt() + + Logger.d("Clicking at display location (pixels) [$displayX, $displayY]") + + val successful = uiDevice.click(displayX, displayY); + + if (!successful) { + throw IllegalArgumentException("Clicking at location [$displayX, $displayY] failed") + } + + delay() + } + fun enterText(text: String, index: Int, keyboardBehavior: KeyboardBehavior) { Logger.d("enterText(text: $text, index: $index)") @@ -262,6 +287,7 @@ class Automator private constructor() { val eY = (uiDevice.displayHeight * endY).roundToInt() val successful = uiDevice.swipe(sX, sY, eX, eY, steps) + if (!successful) { throw IllegalArgumentException("Swipe failed") } diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt index c2edb6640..16e68e0e5 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt @@ -1,5 +1,6 @@ package pl.leancode.patrol +import pl.leancode.patrol.contracts.Contracts import pl.leancode.patrol.contracts.Contracts.ConfigureRequest import pl.leancode.patrol.contracts.Contracts.DarkModeRequest import pl.leancode.patrol.contracts.Contracts.EnterTextRequest @@ -142,6 +143,13 @@ class AutomatorServer(private val automation: Automator) : NativeAutomatorServer ) } + override fun tapAt(request: Contracts.TapAtRequest) { + automation.tapAt( + x = request.x.toFloat(), + y = request.y.toFloat() + ) + } + override fun enterText(request: EnterTextRequest) { if (request.index != null) { automation.enterText( diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt index 83b6daa3b..4900c0ee4 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt @@ -177,6 +177,12 @@ class Contracts { val appId: String ) + data class TapAtRequest ( + val x: Double, + val y: Double, + val appId: String + ) + data class EnterTextRequest ( val data: String, val appId: String, diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt index c553b00be..85f346f82 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt @@ -25,6 +25,7 @@ abstract class NativeAutomatorServer { abstract fun getNativeViews(request: Contracts.GetNativeViewsRequest): Contracts.GetNativeViewsResponse abstract fun tap(request: Contracts.TapRequest) abstract fun doubleTap(request: Contracts.TapRequest) + abstract fun tapAt(request: Contracts.TapAtRequest) abstract fun enterText(request: Contracts.EnterTextRequest) abstract fun swipe(request: Contracts.SwipeRequest) abstract fun waitUntilVisible(request: Contracts.WaitUntilVisibleRequest) @@ -105,6 +106,11 @@ abstract class NativeAutomatorServer { doubleTap(body) Response(OK) }, + "tapAt" bind POST to { + val body = json.fromJson(it.bodyString(), Contracts.TapAtRequest::class.java) + tapAt(body) + Response(OK) + }, "enterText" bind POST to { val body = json.fromJson(it.bodyString(), Contracts.EnterTextRequest::class.java) enterText(body) diff --git a/packages/patrol/lib/src/native/contracts/contracts.dart b/packages/patrol/lib/src/native/contracts/contracts.dart index 599d77066..341a22375 100644 --- a/packages/patrol/lib/src/native/contracts/contracts.dart +++ b/packages/patrol/lib/src/native/contracts/contracts.dart @@ -380,6 +380,31 @@ class TapRequest with EquatableMixin { ]; } +@JsonSerializable() +class TapAtRequest with EquatableMixin { + TapAtRequest({ + required this.x, + required this.y, + required this.appId, + }); + + factory TapAtRequest.fromJson(Map json) => + _$TapAtRequestFromJson(json); + + final double x; + final double y; + final String appId; + + Map toJson() => _$TapAtRequestToJson(this); + + @override + List get props => [ + x, + y, + appId, + ]; +} + @JsonSerializable() class EnterTextRequest with EquatableMixin { EnterTextRequest({ diff --git a/packages/patrol/lib/src/native/contracts/contracts.g.dart b/packages/patrol/lib/src/native/contracts/contracts.g.dart index 62436ad5e..30117098c 100644 --- a/packages/patrol/lib/src/native/contracts/contracts.g.dart +++ b/packages/patrol/lib/src/native/contracts/contracts.g.dart @@ -221,6 +221,19 @@ Map _$TapRequestToJson(TapRequest instance) => 'appId': instance.appId, }; +TapAtRequest _$TapAtRequestFromJson(Map json) => TapAtRequest( + x: (json['x'] as num).toDouble(), + y: (json['y'] as num).toDouble(), + appId: json['appId'] as String, + ); + +Map _$TapAtRequestToJson(TapAtRequest instance) => + { + 'x': instance.x, + 'y': instance.y, + 'appId': instance.appId, + }; + EnterTextRequest _$EnterTextRequestFromJson(Map json) => EnterTextRequest( data: json['data'] as String, diff --git a/packages/patrol/lib/src/native/contracts/native_automator_client.dart b/packages/patrol/lib/src/native/contracts/native_automator_client.dart index d9d8cf49c..313c68ec3 100644 --- a/packages/patrol/lib/src/native/contracts/native_automator_client.dart +++ b/packages/patrol/lib/src/native/contracts/native_automator_client.dart @@ -134,6 +134,15 @@ class NativeAutomatorClient { ); } + Future tapAt( + TapAtRequest request, + ) { + return _sendRequest( + 'tapAt', + request.toJson(), + ); + } + Future enterText( EnterTextRequest request, ) { diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart index ca447c8d9..86522237b 100644 --- a/packages/patrol/lib/src/native/native_automator.dart +++ b/packages/patrol/lib/src/native/native_automator.dart @@ -517,6 +517,24 @@ class NativeAutomator { ); } + /// Taps at a given [location]. + /// + /// [location] must be in the inclusive 0-1 range. + Future tapAt(Offset location, {String? appId}) async { + assert(location.dx >= 0 && location.dx <= 1); + assert(location.dy >= 0 && location.dy <= 1); + + await _wrapRequest('tapAt', () async { + await _client.tapAt( + TapAtRequest( + x: location.dx, + y: location.dy, + appId: appId ?? resolvedAppId, + ), + ); + }); + } + /// Enters text to the native view specified by [selector]. /// /// If the text field isn't visible immediately, this method waits for the From 15eb9c999aad3be967ce4b004eede64dcdaa22ad Mon Sep 17 00:00:00 2001 From: Matej Resetar Date: Thu, 11 Jan 2024 16:01:48 +0100 Subject: [PATCH 2/8] Implement iOS --- dev/e2e_app/integration_test/tap_at_test.dart | 6 +++--- dev/e2e_app/ios/Podfile.lock | 2 +- .../AutomatorServer/Automator/Automator.swift | 1 + .../AutomatorServer/Automator/IOSAutomator.swift | 10 ++++++++++ .../AutomatorServer/Automator/MacOSAutomator.swift | 6 ++++++ .../Classes/AutomatorServer/AutomatorServer.swift | 9 +++++++++ .../darwin/Classes/AutomatorServer/Contracts.swift | 6 ++++++ .../AutomatorServer/NativeAutomatorServer.swift | 12 ++++++++++++ packages/patrol/example/ios/Podfile.lock | 10 +++++----- 9 files changed, 53 insertions(+), 9 deletions(-) diff --git a/dev/e2e_app/integration_test/tap_at_test.dart b/dev/e2e_app/integration_test/tap_at_test.dart index 0511fd081..26e9f996d 100644 --- a/dev/e2e_app/integration_test/tap_at_test.dart +++ b/dev/e2e_app/integration_test/tap_at_test.dart @@ -16,10 +16,10 @@ void main() { await createApp($); await $.native.openApp(appId: appId); - // Native view with the text "Bluetooth" is universally present in the settings app - await $.native.waitUntilVisible(Selector(textContains: 'Bluetooth')); + // Only needed on Android, no wait needed on iOS + await Future.delayed(const Duration(milliseconds: 50)); await $.native.tapAt( - Offset(0.5, 0.5), + Offset(0.5, 0.8), appId: appId, ); }); diff --git a/dev/e2e_app/ios/Podfile.lock b/dev/e2e_app/ios/Podfile.lock index ada012b3f..5e0c8966e 100644 --- a/dev/e2e_app/ios/Podfile.lock +++ b/dev/e2e_app/ios/Podfile.lock @@ -57,4 +57,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: b2bb71756d032256bcb4043384dd40772d5e6a93 -COCOAPODS: 1.14.3 +COCOAPODS: 1.10.1 diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift index db25e6df2..308899bbe 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift @@ -14,6 +14,7 @@ // MARK: General UI interaction func tap(onText text: String, inApp bundleId: String, atIndex index: Int) throws func doubleTap(onText text: String, inApp bundleId: String) throws + func tapAt(coordinate vector: CGVector, inApp bundleId: String) throws func enterText( _ data: String, byText text: String, diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift index 9361231a0..75366c55d 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift @@ -95,6 +95,16 @@ } } + func tapAt(coordinate vector: CGVector, inApp bundleId: String) throws { + try runAction("tapping at coordinate \(vector) in app \(bundleId)") { + let app = try self.getApp(withBundleId: bundleId) + + let coordinate = app.coordinate(withNormalizedOffset: vector) + + coordinate.tap() + } + } + func enterText( _ data: String, byText text: String, diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift index 60c660bb1..acdec8ba8 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift @@ -67,6 +67,12 @@ throw PatrolError.methodNotImplemented("doubleTap") } } + + func tapAt(coordinate vector: CGVector, inApp bundleId: String) throws { + try runAction("tapAt") { + throw PatrolError.methodNotImplemented("tapAt") + } + } func enterText( _ data: String, byText text: String, atIndex index: Int, inApp bundleId: String, diff --git a/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift index 66416ddea..d81e4b9c3 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift @@ -95,6 +95,15 @@ ) } } + + func tapAt(request: TapAtRequest) throws { + return try runCatching { + try automator.tapAt( + coordinate: CGVector(dx: request.x, dy: request.y), + inApp: request.appId + ) + } + } func enterText(request: EnterTextRequest) throws { return try runCatching { diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift b/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift index 3b575e968..205c0cdef 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift @@ -112,6 +112,12 @@ struct TapRequest: Codable { var appId: String } +struct TapAtRequest: Codable { + var x: Double + var y: Double + var appId: String +} + struct EnterTextRequest: Codable { var data: String var appId: String diff --git a/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift b/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift index 7c4060253..f6eb7cf8a 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift @@ -18,6 +18,7 @@ protocol NativeAutomatorServer { func getNativeViews(request: GetNativeViewsRequest) throws -> GetNativeViewsResponse func tap(request: TapRequest) throws func doubleTap(request: TapRequest) throws + func tapAt(request: TapAtRequest) throws func enterText(request: EnterTextRequest) throws func swipe(request: SwipeRequest) throws func waitUntilVisible(request: WaitUntilVisibleRequest) throws @@ -112,6 +113,12 @@ extension NativeAutomatorServer { try doubleTap(request: requestArg) return HTTPResponse(.ok) } + + private func tapAtHandler(request: HTTPRequest) throws -> HTTPResponse { + let requestArg = try JSONDecoder().decode(TapAtRequest.self, from: request.body) + try tapAt(request: requestArg) + return HTTPResponse(.ok) + } private func enterTextHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(EnterTextRequest.self, from: request.body) @@ -303,6 +310,11 @@ extension NativeAutomatorServer { request: request, handler: doubleTapHandler) } + server.route(.POST, "tapAt") { + request in handleRequest( + request: request, + handler: tapAtHandler) + } server.route(.POST, "enterText") { request in handleRequest( request: request, diff --git a/packages/patrol/example/ios/Podfile.lock b/packages/patrol/example/ios/Podfile.lock index cddedcb39..58704e303 100644 --- a/packages/patrol/example/ios/Podfile.lock +++ b/packages/patrol/example/ios/Podfile.lock @@ -14,14 +14,14 @@ PODS: - Firebase/Messaging (10.18.0): - Firebase/CoreOnly - FirebaseMessaging (~> 10.18.0) - - firebase_auth (4.15.3): + - firebase_auth (4.16.0): - Firebase/Auth (= 10.18.0) - firebase_core - Flutter - firebase_core (2.24.2): - Firebase/CoreOnly (= 10.18.0) - Flutter - - firebase_messaging (14.7.9): + - firebase_messaging (14.7.10): - Firebase/Messaging (= 10.18.0) - firebase_core - Flutter @@ -170,9 +170,9 @@ SPEC CHECKSUMS: AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 - firebase_auth: df44e14f8a93e8a9869d91695bd3f8e53d2c9f5a + firebase_auth: 8e9ec02991ca4659111cc671c84d0c010b6bfb26 firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5 - firebase_messaging: 875385354f623750aa03204a028d640108bc3412 + firebase_messaging: 90e8a6db84b6e1e876cebce4f30f01dc495e7014 FirebaseAppCheckInterop: 3cd914842ba46f4304050874cd284de82f154ffd FirebaseAuth: 12314b438fa76048540c8fb86d6cfc9e08595176 FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f @@ -198,4 +198,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a2f999e8fe2642046eaa22133617aca7cd25a681 -COCOAPODS: 1.14.3 +COCOAPODS: 1.10.1 From 2cdbb32538927f2ccbdc6e8f647982b86e61be04 Mon Sep 17 00:00:00 2001 From: Matej Resetar Date: Fri, 12 Jan 2024 10:12:42 +0100 Subject: [PATCH 3/8] Run codegen --- .../patrol/darwin/Classes/AutomatorServer/Contracts.swift | 6 +++--- .../Classes/AutomatorServer/NativeAutomatorServer.swift | 2 +- schema.dart | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift b/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift index 205c0cdef..74d2319a9 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift @@ -113,9 +113,9 @@ struct TapRequest: Codable { } struct TapAtRequest: Codable { - var x: Double - var y: Double - var appId: String + var x: Double + var y: Double + var appId: String } struct EnterTextRequest: Codable { diff --git a/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift b/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift index f6eb7cf8a..08f2e0e65 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift @@ -113,7 +113,7 @@ extension NativeAutomatorServer { try doubleTap(request: requestArg) return HTTPResponse(.ok) } - + private func tapAtHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(TapAtRequest.self, from: request.body) try tapAt(request: requestArg) diff --git a/schema.dart b/schema.dart index 0a7efb8b1..8c39b87e5 100644 --- a/schema.dart +++ b/schema.dart @@ -98,6 +98,12 @@ class TapRequest { late String appId; } +class TapAtRequest { + late double x; + late double y; + late String appId; +} + enum KeyboardBehavior { showAndDismiss, alternative, @@ -191,6 +197,7 @@ abstract class NativeAutomator { GetNativeViewsResponse getNativeViews(GetNativeViewsRequest request); void tap(TapRequest request); void doubleTap(TapRequest request); + void tapAt(TapAtRequest request); void enterText(EnterTextRequest request); void swipe(SwipeRequest request); void waitUntilVisible(WaitUntilVisibleRequest request); From ed5a0d12b30d742ff9bfdccc60e5fd26d283cb53 Mon Sep 17 00:00:00 2001 From: Matej Resetar Date: Fri, 12 Jan 2024 10:28:47 +0100 Subject: [PATCH 4/8] Format Kotlin code with ktlint --- .../android/src/main/kotlin/pl/leancode/patrol/Automator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt index 8b06b5d52..4e43df08c 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt @@ -203,7 +203,7 @@ class Automator private constructor() { Logger.d("Clicking at display location (pixels) [$displayX, $displayY]") - val successful = uiDevice.click(displayX, displayY); + val successful = uiDevice.click(displayX, displayY) if (!successful) { throw IllegalArgumentException("Clicking at location [$displayX, $displayY] failed") From 6620c51810b5d31fb5e097e8650598ce41cc815b Mon Sep 17 00:00:00 2001 From: Matej Resetar Date: Fri, 12 Jan 2024 13:09:53 +0100 Subject: [PATCH 5/8] Add small delay before tapping at coordinate --- dev/e2e_app/integration_test/tap_at_test.dart | 5 ++--- packages/patrol/lib/src/native/native_automator.dart | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dev/e2e_app/integration_test/tap_at_test.dart b/dev/e2e_app/integration_test/tap_at_test.dart index 26e9f996d..608f8dc7e 100644 --- a/dev/e2e_app/integration_test/tap_at_test.dart +++ b/dev/e2e_app/integration_test/tap_at_test.dart @@ -12,12 +12,11 @@ void main() { throw UnsupportedError('Unsupported platform'); } - patrol('taps at the middle of the screen in the Settings app', ($) async { + patrol('taps at the lower middle of the screen in the Settings app', + ($) async { await createApp($); await $.native.openApp(appId: appId); - // Only needed on Android, no wait needed on iOS - await Future.delayed(const Duration(milliseconds: 50)); await $.native.tapAt( Offset(0.5, 0.8), appId: appId, diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart index 86522237b..235d14e3e 100644 --- a/packages/patrol/lib/src/native/native_automator.dart +++ b/packages/patrol/lib/src/native/native_automator.dart @@ -524,6 +524,10 @@ class NativeAutomator { assert(location.dx >= 0 && location.dx <= 1); assert(location.dy >= 0 && location.dy <= 1); + // Needed for an edge case observed on Android where if a newly opened app + // updates its layout right after being launched, tapping without delay fails + await Future.delayed(const Duration(milliseconds: 5)); + await _wrapRequest('tapAt', () async { await _client.tapAt( TapAtRequest( From a8cc7e19de84e37e14d585fb51c5b4a0df0dd1b3 Mon Sep 17 00:00:00 2001 From: Matej Resetar Date: Fri, 12 Jan 2024 15:26:05 +0100 Subject: [PATCH 6/8] Fix formatting --- .../kotlin/pl/leancode/patrol/Automator.kt | 25 ------------------- .../Automator/IOSAutomator.swift | 18 ++++++------- 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt index efc033815..68fc7e25e 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt @@ -212,31 +212,6 @@ class Automator private constructor() { delay() } - fun tapAt(x: Float, y: Float) { - Logger.d("tapAt(x: $x, y: $y)") - - if (x !in 0f..1f) { - throw IllegalArgumentException("x represents a percentage and must be between 0 and 1") - } - - if (y !in 0f..1f) { - throw IllegalArgumentException("y represents a percentage and must be between 0 and 1") - } - - val displayX = (uiDevice.displayWidth * x).roundToInt() - val displayY = (uiDevice.displayHeight * y).roundToInt() - - Logger.d("Clicking at display location (pixels) [$displayX, $displayY]") - - val successful = uiDevice.click(displayX, displayY) - - if (!successful) { - throw IllegalArgumentException("Clicking at location [$displayX, $displayY] failed") - } - - delay() - } - fun enterText(text: String, index: Int, keyboardBehavior: KeyboardBehavior, timeout: Long? = null) { Logger.d("enterText(text: $text, index: $index)") diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift index 8328d8287..5e2996e2f 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift @@ -102,15 +102,15 @@ } } - func tapAt(coordinate vector: CGVector, inApp bundleId: String) throws { - try runAction("tapping at coordinate \(vector) in app \(bundleId)") { - let app = try self.getApp(withBundleId: bundleId) - - let coordinate = app.coordinate(withNormalizedOffset: vector) - - coordinate.tap() - } - } + func tapAt(coordinate vector: CGVector, inApp bundleId: String) throws { + try runAction("tapping at coordinate \(vector) in app \(bundleId)") { + let app = try self.getApp(withBundleId: bundleId) + + let coordinate = app.coordinate(withNormalizedOffset: vector) + + coordinate.tap() + } + } func enterText( _ data: String, From 7786ceaae7c7c79b660c5d2329ba8b54b8302875 Mon Sep 17 00:00:00 2001 From: Matej Resetar Date: Fri, 12 Jan 2024 15:31:03 +0100 Subject: [PATCH 7/8] Update changelog and docs --- dev/e2e_app/ios/Podfile.lock | 2 +- docs/native/feature-parity.mdx | 50 ++++++++++++++++++---------------- packages/patrol/CHANGELOG.md | 1 + 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/dev/e2e_app/ios/Podfile.lock b/dev/e2e_app/ios/Podfile.lock index 5e0c8966e..ada012b3f 100644 --- a/dev/e2e_app/ios/Podfile.lock +++ b/dev/e2e_app/ios/Podfile.lock @@ -57,4 +57,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: b2bb71756d032256bcb4043384dd40772d5e6a93 -COCOAPODS: 1.10.1 +COCOAPODS: 1.14.3 diff --git a/docs/native/feature-parity.mdx b/docs/native/feature-parity.mdx index 3bf87e150..c3fa741f4 100644 --- a/docs/native/feature-parity.mdx +++ b/docs/native/feature-parity.mdx @@ -10,37 +10,38 @@ implemented. We hope that it will help you evaluate Patrol. We strive for high feature parity across iOS and Android, but in some cases it's impossible to reach 100%. macOs support is still in alpha, so it has no native features yet. - ### Table | **Feature** | **Android** | **iOS** | **macOS (alpha)** | | --------------------------- | -------------- | ------------- | ----------------- | -| [Press home] | ✅ | ✅ | ❌ | -| [Press back] | ✅ | ❌ (no API) | ❌ | -| [Open any app] | ✅ | ✅ | ✅ | -| [Open notifications] | ✅ | ✅ | ❌ | -| [Tap on notification] | ✅ | ✅ | ❌ | -| [Open quick settings] | ✅ | ✅ | ❌ | -| [Toggle dark mode] | ✅ | ✅ | ❌ | -| [Toggle airplane mode] | ✅ see [#1359] | ✅ | ❌ | -| [Toggle cellular] | ✅ | ✅ | ❌ | -| [Toggle Wi-Fi] | ✅ | ✅ | ❌ | -| [Toggle Bluetooth] | ✅ see [#282] | ✅ | ❌ | -| Toggle location | ✅ see [#283] | ✅ see [#326] | ❌ | -| [Tap] | ✅ | ✅ | ❌ | -| [Double tap] | ✅ | ✅ | ❌ | -| [Enter text] | ✅ | ✅ | ❌ | -| [Swipe] | ✅ | ✅ | ❌ | -| [Handle permission dialogs] | ✅ | ✅ | ❌ | -| Interact with WebView | ⚠️ see [#244] | ✅ | ❌ | +| [Press home] | ✅ | ✅ | ❌ | +| [Press back] | ✅ | ❌ (no API) | ❌ | +| [Open any app] | ✅ | ✅ | ✅ | +| [Open notifications] | ✅ | ✅ | ❌ | +| [Tap on notification] | ✅ | ✅ | ❌ | +| [Open quick settings] | ✅ | ✅ | ❌ | +| [Toggle dark mode] | ✅ | ✅ | ❌ | +| [Toggle airplane mode] | ✅ see [#1359] | ✅ | ❌ | +| [Toggle cellular] | ✅ | ✅ | ❌ | +| [Toggle Wi-Fi] | ✅ | ✅ | ❌ | +| [Toggle Bluetooth] | ✅ see [#282] | ✅ | ❌ | +| Toggle location | ✅ see [#283] | ✅ see [#326] | ❌ | +| [Tap] | ✅ | ✅ | ❌ | +| [Double tap] | ✅ | ✅ | ❌ | +| [Tap at coordinate] | ✅ | ✅ | ❌ | +| [Enter text] | ✅ | ✅ | ❌ | +| [Swipe] | ✅ | ✅ | ❌ | +| [Handle permission dialogs] | ✅ | ✅ | ❌ | +| Interact with WebView | ⚠️ see [#244] | ✅ | ❌ | ### Platfom support Patrol works on: - - Android 5.0 (API 21) and newer, - - iOS 11 and newer, - - macOS 10.14 and newer. - + +- Android 5.0 (API 21) and newer, +- iOS 11 and newer, +- macOS 10.14 and newer. + On mobile platforms it works on both physical and virtual devices. [#244]: https://github.com/leancodepl/patrol/issues/244 @@ -61,6 +62,7 @@ On mobile platforms it works on both physical and virtual devices. [toggle bluetooth]: https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator/enableBluetooth.html [tap]: https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator/tap.html [double tap]: https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator/doubleTap.html +[tap at coordinate]: https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator/tapAt.html [enter text]: https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator/enterText.html -[enter text]: https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator/swipe.html +[swipe]: https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator/swipe.html [handle permission dialogs]: https://pub.dev/documentation/patrol/latest/patrol/NativeAutomator/grantPermissionWhenInUse.html diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 2d6041a7f..8c14ce328 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased - Add optional timeout parameter to native methods (#2042). +- Add `$.native.tapAt()` (#2053) ## 3.4.0 From 2c12384d50ff8c98de045551610f66307a0b73e3 Mon Sep 17 00:00:00 2001 From: Matej Resetar Date: Fri, 12 Jan 2024 16:39:20 +0100 Subject: [PATCH 8/8] Format Swift --- .../AutomatorServer/Automator/IOSAutomator.swift | 14 +++++++------- .../Automator/MacOSAutomator.swift | 10 +++++----- .../AutomatorServer/AutomatorServer.swift | 16 ++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift index 5e2996e2f..6e53581b8 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift @@ -103,13 +103,13 @@ } func tapAt(coordinate vector: CGVector, inApp bundleId: String) throws { - try runAction("tapping at coordinate \(vector) in app \(bundleId)") { - let app = try self.getApp(withBundleId: bundleId) - - let coordinate = app.coordinate(withNormalizedOffset: vector) - - coordinate.tap() - } + try runAction("tapping at coordinate \(vector) in app \(bundleId)") { + let app = try self.getApp(withBundleId: bundleId) + + let coordinate = app.coordinate(withNormalizedOffset: vector) + + coordinate.tap() + } } func enterText( diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift index f8d1f332e..b8ebc6c14 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift @@ -72,12 +72,12 @@ throw PatrolError.methodNotImplemented("doubleTap") } } - - func tapAt(coordinate vector: CGVector, inApp bundleId: String) throws { - try runAction("tapAt") { - throw PatrolError.methodNotImplemented("tapAt") - } + + func tapAt(coordinate vector: CGVector, inApp bundleId: String) throws { + try runAction("tapAt") { + throw PatrolError.methodNotImplemented("tapAt") } + } func enterText( _ data: String, diff --git a/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift index 8d4a9b551..7f904ef52 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift @@ -97,15 +97,15 @@ ) } } - - func tapAt(request: TapAtRequest) throws { - return try runCatching { - try automator.tapAt( - coordinate: CGVector(dx: request.x, dy: request.y), - inApp: request.appId - ) - } + + func tapAt(request: TapAtRequest) throws { + return try runCatching { + try automator.tapAt( + coordinate: CGVector(dx: request.x, dy: request.y), + inApp: request.appId + ) } + } func enterText(request: EnterTextRequest) throws { return try runCatching {