diff --git a/.github/workflows/test-ios-device.yaml b/.github/workflows/test-ios-device.yaml index e197212ef..3ca7c1296 100644 --- a/.github/workflows/test-ios-device.yaml +++ b/.github/workflows/test-ios-device.yaml @@ -92,6 +92,7 @@ jobs: TESTS_TO_EXCLUDE="android_app_test,\ permissions/permissions_location_test,\ service_dark_mode_test,\ + service_location_test,\ permissions/notifications_test" target_paths="" diff --git a/.github/workflows/test-ios-simulator.yaml b/.github/workflows/test-ios-simulator.yaml index 901468be7..2d5921f4c 100644 --- a/.github/workflows/test-ios-simulator.yaml +++ b/.github/workflows/test-ios-simulator.yaml @@ -79,6 +79,7 @@ jobs: service_bluetooth_test,\ service_cellular_test,\ service_wifi_test,\ + service_location_test,\ webview_stackoverflow_test,\ webview_leancode_test,\ webview_hackernews_test" diff --git a/dev/e2e_app/integration_test/service_location_test.dart b/dev/e2e_app/integration_test/service_location_test.dart new file mode 100644 index 000000000..0d4660770 --- /dev/null +++ b/dev/e2e_app/integration_test/service_location_test.dart @@ -0,0 +1,12 @@ +import 'common.dart'; + +void main() { + patrol('disables and enables location twice', ($) async { + await createApp($); + + await $.native.disableLocation(); + await $.native.enableLocation(); + await $.native.disableLocation(); + await $.native.enableLocation(); + }); +} diff --git a/docs/native/feature-parity.mdx b/docs/native/feature-parity.mdx index 6215a8528..f694a05ae 100644 --- a/docs/native/feature-parity.mdx +++ b/docs/native/feature-parity.mdx @@ -24,7 +24,7 @@ impossible to reach 100%. macOS support is still in alpha, so it has no native f | [Toggle cellular] | ✅ | ✅ | ❌ | | [Toggle Wi-Fi] | ✅ | ✅ | ❌ | | [Toggle Bluetooth] | ✅ | ✅ | ❌ | -| Toggle location | ✅ see [#283] | ✅ see [#326] | ❌ | +| Toggle location | ✅ | ✅ see [#326] | ❌ | | [Tap] | ✅ | ✅ | ❌ | | [Double tap] | ✅ | ✅ | ❌ | | [Tap at coordinate] | ✅ | ✅ | ❌ | diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 237296efc..beec35e20 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -2,6 +2,7 @@ - Implement `enableBluetooth` and `disableBluetooth` methods for Android > 11. (#2254) - Implement `enableAirplaneMode` and `disableAirplaneMode` methods for Android. (#2254) +- Implement `enableLocation` and `disableLocation` methods for Android. (#2259) ## 3.9.0 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 09b7ae79f..fd17c45e0 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 @@ -4,6 +4,7 @@ import android.app.Instrumentation import android.app.UiAutomation import android.content.Context import android.content.Intent +import android.location.LocationManager import android.net.Uri import android.os.Build import android.os.SystemClock @@ -223,6 +224,42 @@ class Automator private constructor() { } } + fun enableLocation() { + val enabled = isLocationEnabled() + if (enabled) { + Logger.d("Location already enabled") + return + } else { + toggleLocation() + } + } + + fun disableLocation() { + val enabled = isLocationEnabled() + if (!enabled) { + Logger.d("Location already disabled") + return + } else { + toggleLocation() + } + } + + private fun isLocationEnabled(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // This is a new method provided in API 28 + val lm = targetContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager + lm.isLocationEnabled + } else { + // This was deprecated in API 28 + val mode = Settings.Secure.getInt( + targetContext.contentResolver, + Settings.Secure.LOCATION_MODE, + Settings.Secure.LOCATION_MODE_OFF + ) + mode != Settings.Secure.LOCATION_MODE_OFF + } + } + fun getNativeViews(selector: BySelector): List { Logger.d("getNativeViews()") @@ -714,6 +751,23 @@ class Automator private constructor() { } } + private fun toggleLocation() { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + targetContext.startActivity(intent) + + var uiSelector = UiSelector() + uiSelector = uiSelector.text("Use location") + val uiObject = uiDevice.findObject(uiSelector) + if (uiObject != null) { + uiObject.click() + pressBack() + delay() + } else { + throw PatrolException("Could not find location toggle") + } + } + companion object { val instance = Automator() } 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 221961339..20e58dda8 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 @@ -132,6 +132,14 @@ class AutomatorServer(private val automation: Automator) : NativeAutomatorServer automation.disableBluetooth() } + override fun enableLocation() { + automation.enableLocation() + } + + override fun disableLocation() { + automation.disableLocation() + } + override fun getNativeViews(request: GetNativeViewsRequest): GetNativeViewsResponse { if (request.selector != null) { val views = automation.getNativeViews(request.selector.toBySelector()) 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 6e23f1d6f..ade144b74 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 @@ -42,6 +42,8 @@ abstract class NativeAutomatorServer { abstract fun disableBluetooth() abstract fun enableDarkMode(request: Contracts.DarkModeRequest) abstract fun disableDarkMode(request: Contracts.DarkModeRequest) + abstract fun enableLocation() + abstract fun disableLocation() abstract fun openNotifications() abstract fun closeNotifications() abstract fun closeHeadsUpNotification() @@ -184,6 +186,14 @@ abstract class NativeAutomatorServer { disableDarkMode(body) Response(OK) }, + "enableLocation" bind POST to { + enableLocation() + Response(OK) + }, + "disableLocation" bind POST to { + disableLocation() + Response(OK) + }, "openNotifications" bind POST to { openNotifications() Response(OK) diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift index 40a1c0e4c..5334f28a3 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift @@ -250,6 +250,8 @@ extension Selector { // MARK: Services func enableDarkMode(_ bundleId: String) throws func disableDarkMode(_ bundleId: String) throws + func enableLocation() throws + func disableLocation() throws func enableAirplaneMode() throws func disableAirplaneMode() throws func enableCellular() throws diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift index 76c2e64b2..0db23d1be 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift @@ -415,6 +415,18 @@ } } + func enableLocation() throws { + try runAction("enableLocation") { + throw PatrolError.methodNotImplemented("enableLocation") + } + } + + func disableLocation() throws { + try runAction("disableLocation") { + throw PatrolError.methodNotImplemented("disableLocation") + } + } + func enableAirplaneMode() throws { try runControlCenterAction("enabling airplane mode") { let toggle = self.springboard.switches["airplane-mode-button"] diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift index c345f6f86..930af1b6a 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/MacOSAutomator.swift @@ -209,6 +209,18 @@ } } + func enableLocation() throws { + try runAction("enableLocation") { + throw PatrolError.methodNotImplemented("enableLocation") + } + } + + func disableLocation() throws { + try runAction("disableLocation") { + throw PatrolError.methodNotImplemented("disableLocation") + } + } + func enableAirplaneMode() throws { try runAction("enableAirplaneMode") { throw PatrolError.methodNotImplemented("enableAirplaneMode") diff --git a/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift index 33624d4ef..a18c69212 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift @@ -283,6 +283,18 @@ } } + func enableBluetooth() throws { + return try runCatching { + try automator.enableLocation() + } + } + + func disableBluetooth() throws { + return try runCatching { + try automator.disableLocation() + } + } + // MARK: Notifications func openNotifications() throws { diff --git a/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift b/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift index 9d4e60dc3..d02998b9b 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift @@ -35,6 +35,8 @@ protocol NativeAutomatorServer { func disableBluetooth() throws func enableDarkMode(request: DarkModeRequest) throws func disableDarkMode(request: DarkModeRequest) throws + func enableLocation() throws + func disableLocation() throws func openNotifications() throws func closeNotifications() throws func closeHeadsUpNotification() throws @@ -209,6 +211,16 @@ extension NativeAutomatorServer { return HTTPResponse(.ok) } + private func enableLocationHandler(request: HTTPRequest) throws -> HTTPResponse { + try enableLocation() + return HTTPResponse(.ok) + } + + private func disableLocationHandler(request: HTTPRequest) throws -> HTTPResponse { + try disableLocation() + return HTTPResponse(.ok) + } + private func openNotificationsHandler(request: HTTPRequest) throws -> HTTPResponse { try openNotifications() return HTTPResponse(.ok) @@ -414,6 +426,16 @@ extension NativeAutomatorServer { request: request, handler: disableDarkModeHandler) } + server.route(.POST, "enableLocation") { + request in handleRequest( + request: request, + handler: enableLocationHandler) + } + server.route(.POST, "disableLocation") { + request in handleRequest( + request: request, + handler: disableLocationHandler) + } server.route(.POST, "openNotifications") { request in handleRequest( request: request, 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 957b367a0..9f8a580b2 100644 --- a/packages/patrol/lib/src/native/contracts/native_automator_client.dart +++ b/packages/patrol/lib/src/native/contracts/native_automator_client.dart @@ -257,6 +257,18 @@ class NativeAutomatorClient { ); } + Future enableLocation() { + return _sendRequest( + 'enableLocation', + ); + } + + Future disableLocation() { + return _sendRequest( + 'disableLocation', + ); + } + Future openNotifications() { return _sendRequest( 'openNotifications', diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart index 635d13d3a..ed728a6a6 100644 --- a/packages/patrol/lib/src/native/native_automator.dart +++ b/packages/patrol/lib/src/native/native_automator.dart @@ -554,6 +554,28 @@ class NativeAutomator { await _wrapRequest('disableBluetooth', _client.disableBluetooth); } + /// Enables location. + /// + /// On Android, opens the location settings screen and toggles the location + /// switch to enable location. + /// If the location already enabled, it does nothing. + /// + /// Doesn't work for iOS. + Future enableLocation() async { + await _wrapRequest('enableLocation', _client.enableLocation); + } + + /// Disables location. + /// + /// On Android, opens the location settings screen and toggles the location + /// switch to disable location. + /// If the location already enabled, it does nothing. + /// + /// Doesn't work for iOS. + Future disableLocation() async { + await _wrapRequest('disableLocation', _client.disableLocation); + } + /// Taps on the native view specified by [selector]. /// /// It waits for the view to become visible for [timeout] duration. If diff --git a/packages/patrol/lib/src/native/native_automator2.dart b/packages/patrol/lib/src/native/native_automator2.dart index 6ab8fd09d..28b789eaa 100644 --- a/packages/patrol/lib/src/native/native_automator2.dart +++ b/packages/patrol/lib/src/native/native_automator2.dart @@ -432,6 +432,28 @@ class NativeAutomator2 { await _wrapRequest('disableBluetooth', _client.disableBluetooth); } + /// Enables location. + /// + /// On Android, opens the location settings screen and toggles the location + /// switch to enable location. + /// If the location already enabled, it does nothing. + /// + /// Doesn't work for iOS. + Future enableLocation() async { + await _wrapRequest('enableLocation', _client.enableLocation); + } + + /// Disables location. + /// + /// On Android, opens the location settings screen and toggles the location + /// switch to disable location. + /// If the location already enabled, it does nothing. + /// + /// Doesn't work for iOS. + Future disableLocation() async { + await _wrapRequest('disableLocation', _client.disableLocation); + } + /// Taps on the native view specified by [selector]. /// /// It waits for the view to become visible for [timeout] duration. If diff --git a/schema.dart b/schema.dart index a851c6da3..d1903b6d3 100644 --- a/schema.dart +++ b/schema.dart @@ -332,6 +332,8 @@ abstract class NativeAutomator { void disableBluetooth(); void enableDarkMode(DarkModeRequest request); void disableDarkMode(DarkModeRequest request); + void enableLocation(); + void disableLocation(); // notifications void openNotifications();