From 7eff73739c181fa09354f5fc1e546b3bb0f8fba1 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Fri, 6 Oct 2023 10:26:36 +0200 Subject: [PATCH 01/21] update outdated docs of the patrol build command --- docs/cli-commands/build.mdx | 47 +++++++++++-------------------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/docs/cli-commands/build.mdx b/docs/cli-commands/build.mdx index aa699fe06..2cf6c7582 100644 --- a/docs/cli-commands/build.mdx +++ b/docs/cli-commands/build.mdx @@ -34,7 +34,7 @@ Test Lab. It works the same as `patrol test`, except that it does run tests. ### Examples -**To build for Android in debug mode** +**To build a single test for Android in debug mode** ``` patrol build android --target integration_test/example_test.dart @@ -46,48 +46,29 @@ or alternatively (but redundantly): patrol build android --target integration_test/example_test.dart --debug ``` -**To build for iOS device in release mode** +**To build all tests for Android in debug mode** ``` -patrol build ios --target integration_test/example_test.dart --release +patrol build android ``` -**To build for iOS simulator in debug mode** +**To build a single test for iOS device in release mode** ``` -patrol build ios --target integration_test/example_test.dart --debug -``` - -### Caveats - -Only a single test target file can be built. For example, consider the following -`integration_test` directory containing 4 test targets: - -``` -integration_test/ -├── common.dart -├── example_test.dart -├── notifications_test.dart -├── permissions_location_test.dart -├── webview_login_test.dart +patrol build ios --target integration_test/example_test.dart --release ``` -Dart file must end with `_test.dart` to be considered to be a test. - -To build the app for testing on Android, you have to run: +**To build a single test for iOS simulator in debug mode** ``` -patrol build android --target +patrol build ios --target integration_test/example_test.dart --debug ``` -4 times, each time with a different `--target`, to get 4 different app binaries. - -These 4 different binaries are almost the same – the only difference is the -entrypoint. This is needlessly inefficient for apps with many tests, but it's a -design flaw in the way integration testing in Flutter works. - -[We know about this issue][bundling_issue] and we're aiming to fix it soon with -an approach we call "test bundling". This will dramatically speed up testing -apps with many integration tests. +### Under the hood -[bundling_issue]: https://github.com/leancodepl/patrol/issues/1004 +The `patrol build` command walks through hierarchy of the `integration_test` +directory and finds all files that end with `_test.dart`, and then creates an +additional "test bundle" file that references all the tests it found. Thanks to +this, all tests are built into a single app binary - only a single build is +required. If you're curious to learn more about this problem, see [Flutter issue +#115751](https://github.com/flutter/flutter/issues/115751) From 98165b514ff212a53e76aa82e20a89f7b20bf199 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Mon, 9 Oct 2023 17:37:48 +0200 Subject: [PATCH 02/21] Change kotlinx.serialization to Gson --- .../pl/leancode/patrol/PatrolServerFilters.kt | 3 -- .../pl/leancode/patrol/contracts/Contracts.kt | 31 ------------- .../patrol/contracts/NativeAutomatorServer.kt | 43 +++++++++---------- .../contracts/PatrolAppServiceClient.kt | 11 +++-- .../android/android_contracts_generator.dart | 4 -- .../android_http4k_client_generator.dart | 9 ++-- .../android_http4k_server_generator.dart | 9 ++-- 7 files changed, 34 insertions(+), 76 deletions(-) diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolServerFilters.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolServerFilters.kt index 55d71f44e..2b488bee0 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolServerFilters.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolServerFilters.kt @@ -1,15 +1,12 @@ package pl.leancode.patrol import androidx.test.uiautomator.UiObjectNotFoundException -import kotlinx.serialization.json.Json import org.http4k.core.Filter import org.http4k.core.Response import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR import org.http4k.core.Status.Companion.NOT_FOUND import org.http4k.core.Status.Companion.NOT_IMPLEMENTED -private val json = Json { ignoreUnknownKeys = true } - val printer = Filter { next -> { request -> 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 cf0ac5b2d..820eca3f8 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 @@ -5,59 +5,48 @@ package pl.leancode.patrol.contracts; -import kotlinx.serialization.Serializable - class Contracts { - @Serializable enum class GroupEntryType { group, test, } - @Serializable enum class RunDartTestResponseResult { success, skipped, failure, } - @Serializable enum class KeyboardBehavior { showAndDismiss, alternative, } - @Serializable enum class HandlePermissionRequestCode { whileUsing, onlyThisTime, denied, } - @Serializable enum class SetLocationAccuracyRequestLocationAccuracy { coarse, fine, } - @Serializable data class DartGroupEntry ( val name: String, val type: GroupEntryType, val entries: List ) - @Serializable data class ListDartTestsResponse ( val group: DartGroupEntry ) - @Serializable data class RunDartTestRequest ( val name: String ) - @Serializable data class RunDartTestResponse ( val result: RunDartTestResponseResult, val details: String? = null @@ -67,22 +56,18 @@ class Contracts { } } - @Serializable data class ConfigureRequest ( val findTimeoutMillis: Long ) - @Serializable data class OpenAppRequest ( val appId: String ) - @Serializable class OpenQuickSettingsRequest ( ) - @Serializable data class Selector ( val text: String? = null, val textStartsWith: String? = null, @@ -135,13 +120,11 @@ class Contracts { } } - @Serializable data class GetNativeViewsRequest ( val selector: Selector, val appId: String ) - @Serializable data class NativeView ( val className: String? = null, val text: String? = null, @@ -173,18 +156,15 @@ class Contracts { } } - @Serializable data class GetNativeViewsResponse ( val nativeViews: List ) - @Serializable data class TapRequest ( val selector: Selector, val appId: String ) - @Serializable data class EnterTextRequest ( val data: String, val appId: String, @@ -200,7 +180,6 @@ class Contracts { } } - @Serializable data class SwipeRequest ( val startX: Double, val startY: Double, @@ -209,18 +188,15 @@ class Contracts { val steps: Long ) - @Serializable data class WaitUntilVisibleRequest ( val selector: Selector, val appId: String ) - @Serializable data class DarkModeRequest ( val appId: String ) - @Serializable data class Notification ( val appName: String? = null, val title: String, @@ -235,17 +211,14 @@ class Contracts { } } - @Serializable data class GetNotificationsResponse ( val notifications: List ) - @Serializable class GetNotificationsRequest ( ) - @Serializable data class TapOnNotificationRequest ( val index: Long? = null, val selector: Selector? = null @@ -258,22 +231,18 @@ class Contracts { } } - @Serializable data class PermissionDialogVisibleResponse ( val visible: Boolean ) - @Serializable data class PermissionDialogVisibleRequest ( val timeoutMillis: Long ) - @Serializable data class HandlePermissionRequest ( val code: HandlePermissionRequestCode ) - @Serializable data class SetLocationAccuracyRequest ( val locationAccuracy: SetLocationAccuracyRequestLocationAccuracy ) 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 a61c499a9..617aee7cc 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 @@ -5,13 +5,12 @@ package pl.leancode.patrol.contracts; +import com.google.gson.Gson import org.http4k.core.Response import org.http4k.core.Method.POST import org.http4k.routing.bind import org.http4k.core.Status.Companion.OK import org.http4k.routing.routes -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json abstract class NativeAutomatorServer { abstract fun initialize() @@ -55,7 +54,7 @@ abstract class NativeAutomatorServer { Response(OK) }, "configure" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.ConfigureRequest::class.java) configure(body) Response(OK) }, @@ -76,42 +75,42 @@ abstract class NativeAutomatorServer { Response(OK) }, "openApp" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.OpenAppRequest::class.java) openApp(body) Response(OK) }, "openQuickSettings" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.OpenQuickSettingsRequest::class.java) openQuickSettings(body) Response(OK) }, "getNativeViews" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.GetNativeViewsRequest::class.java) val response = getNativeViews(body) - Response(OK).body(json.encodeToString(response)) + Response(OK).body(json.toJson(response)) }, "tap" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.TapRequest::class.java) tap(body) Response(OK) }, "doubleTap" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.TapRequest::class.java) doubleTap(body) Response(OK) }, "enterText" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.EnterTextRequest::class.java) enterText(body) Response(OK) }, "swipe" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.SwipeRequest::class.java) swipe(body) Response(OK) }, "waitUntilVisible" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.WaitUntilVisibleRequest::class.java) waitUntilVisible(body) Response(OK) }, @@ -148,12 +147,12 @@ abstract class NativeAutomatorServer { Response(OK) }, "enableDarkMode" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.DarkModeRequest::class.java) enableDarkMode(body) Response(OK) }, "disableDarkMode" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.DarkModeRequest::class.java) disableDarkMode(body) Response(OK) }, @@ -170,27 +169,27 @@ abstract class NativeAutomatorServer { Response(OK) }, "getNotifications" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.GetNotificationsRequest::class.java) val response = getNotifications(body) - Response(OK).body(json.encodeToString(response)) + Response(OK).body(json.toJson(response)) }, "tapOnNotification" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.TapOnNotificationRequest::class.java) tapOnNotification(body) Response(OK) }, "isPermissionDialogVisible" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.PermissionDialogVisibleRequest::class.java) val response = isPermissionDialogVisible(body) - Response(OK).body(json.encodeToString(response)) + Response(OK).body(json.toJson(response)) }, "handlePermissionDialog" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.HandlePermissionRequest::class.java) handlePermissionDialog(body) Response(OK) }, "setLocationAccuracy" bind POST to { - val body = json.decodeFromString(it.bodyString()) + val body = json.fromJson(it.bodyString(), Contracts.SetLocationAccuracyRequest::class.java) setLocationAccuracy(body) Response(OK) }, @@ -204,6 +203,6 @@ abstract class NativeAutomatorServer { } ) - private val json = Json { ignoreUnknownKeys = true } + private val json = Gson() } diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt index 5f39bd12e..3896e9f77 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt @@ -5,24 +5,23 @@ package pl.leancode.patrol.contracts; +import com.google.gson.Gson import com.squareup.okhttp.MediaType import com.squareup.okhttp.OkHttpClient import com.squareup.okhttp.Request import com.squareup.okhttp.RequestBody -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import java.util.concurrent.TimeUnit class PatrolAppServiceClient(address: String, port: Int, private val timeout: Long, private val timeUnit: TimeUnit) { fun listDartTests(): Contracts.ListDartTestsResponse { val response = performRequest("listDartTests") - return json.decodeFromString(response) + return json.fromJson(response, Contracts.ListDartTestsResponse::class.java) } fun runDartTest(request: Contracts.RunDartTestRequest): Contracts.RunDartTestResponse { - val response = performRequest("runDartTest", json.encodeToString(request)) - return json.decodeFromString(response) + val response = performRequest("runDartTest", json.toJson(request)) + return json.fromJson(response, Contracts.RunDartTestResponse::class.java) } private fun performRequest(path: String, requestBody: String? = null): String { @@ -53,7 +52,7 @@ class PatrolAppServiceClient(address: String, port: Int, private val timeout: Lo val serverUrl = "http://$address:$port/" - private val json = Json { ignoreUnknownKeys = true } + private val json = Gson() private val jsonMediaType = MediaType.parse("application/json; charset=utf-8") } diff --git a/packages/patrol_gen/lib/src/generators/android/android_contracts_generator.dart b/packages/patrol_gen/lib/src/generators/android/android_contracts_generator.dart index d18a113a0..c2a1ca671 100644 --- a/packages/patrol_gen/lib/src/generators/android/android_contracts_generator.dart +++ b/packages/patrol_gen/lib/src/generators/android/android_contracts_generator.dart @@ -33,8 +33,6 @@ class AndroidContractsGenerator { package ${config.package}; -import kotlinx.serialization.Serializable - '''; } @@ -59,7 +57,6 @@ $optionalFieldUtils } return ''' - @Serializable ${dataKeyword}class ${message.name} ( $fields )$optionalFieldUtils @@ -77,7 +74,6 @@ $fields final cases = enumDefinition.fields.map((e) => ' $e,').join('\n'); return ''' - @Serializable enum class ${enumDefinition.name} { $cases } diff --git a/packages/patrol_gen/lib/src/generators/android/android_http4k_client_generator.dart b/packages/patrol_gen/lib/src/generators/android/android_http4k_client_generator.dart index 2ecf09a90..d4ce57e3a 100644 --- a/packages/patrol_gen/lib/src/generators/android/android_http4k_client_generator.dart +++ b/packages/patrol_gen/lib/src/generators/android/android_http4k_client_generator.dart @@ -25,12 +25,11 @@ class AndroidHttp4kClientGenerator { package ${config.package}; +import com.google.gson.Gson import com.squareup.okhttp.MediaType import com.squareup.okhttp.OkHttpClient import com.squareup.okhttp.Request import com.squareup.okhttp.RequestBody -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import java.util.concurrent.TimeUnit '''; @@ -77,7 +76,7 @@ $endpoints val serverUrl = $url - private val json = Json { ignoreUnknownKeys = true } + private val json = Gson() private val jsonMediaType = MediaType.parse("application/json; charset=utf-8") }'''; @@ -92,12 +91,12 @@ $endpoints : ''; final serializeParameter = - endpoint.request != null ? ', json.encodeToString(request)' : ''; + endpoint.request != null ? ', json.toJson(request)' : ''; final body = endpoint.response != null ? ''' val response = performRequest("${endpoint.name}"$serializeParameter) - return json.decodeFromString(response)''' + return json.fromJson(response, Contracts.${endpoint.response!.name}::class.java)''' : ''' return performRequest("${endpoint.name}"$serializeParameter)'''; diff --git a/packages/patrol_gen/lib/src/generators/android/android_http4k_server_generator.dart b/packages/patrol_gen/lib/src/generators/android/android_http4k_server_generator.dart index 20277c235..44d5abba7 100644 --- a/packages/patrol_gen/lib/src/generators/android/android_http4k_server_generator.dart +++ b/packages/patrol_gen/lib/src/generators/android/android_http4k_server_generator.dart @@ -23,13 +23,12 @@ class AndroidHttp4kServerGenerator { package ${config.package}; +import com.google.gson.Gson import org.http4k.core.Response import org.http4k.core.Method.POST import org.http4k.routing.bind import org.http4k.core.Status.Companion.OK import org.http4k.routing.routes -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json '''; } @@ -46,7 +45,7 @@ $handlers $routes ) - private val json = Json { ignoreUnknownKeys = true } + private val json = Gson() } '''; } @@ -56,12 +55,12 @@ $routes final requestDeserialization = e.request != null ? ''' - val body = json.decodeFromString(it.bodyString())''' + val body = json.fromJson(it.bodyString(), Contracts.${e.request!.name}::class.java)''' : ''; final requestArg = e.request != null ? 'body' : ''; final responseSerialization = - e.response != null ? '.body(json.encodeToString(response))' : ''; + e.response != null ? '.body(json.toJson(response))' : ''; final responseVariable = e.response != null ? 'val response = ' : ''; From e5849794f8d0344f923433801762356be9dccb54 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Mon, 9 Oct 2023 17:43:15 +0200 Subject: [PATCH 03/21] Update build.gradle --- packages/patrol/android/build.gradle | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/patrol/android/build.gradle b/packages/patrol/android/build.gradle index 681dd2537..e7e0b461c 100644 --- a/packages/patrol/android/build.gradle +++ b/packages/patrol/android/build.gradle @@ -13,14 +13,12 @@ buildscript { classpath "com.android.tools.build:gradle:7.4.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jlleitschuh.gradle:ktlint-gradle:11.5.0" - classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } apply plugin: "com.android.library" apply plugin: "kotlin-android" apply plugin: "org.jlleitschuh.gradle.ktlint" -apply plugin: "kotlinx-serialization" ktlint { filter { @@ -69,7 +67,7 @@ android { implementation "org.http4k:http4k-core" implementation "com.squareup.okhttp:okhttp:2.7.5" // See https://github.com/square/okhttp/issues/8031 implementation "org.http4k:http4k-server-ktorcio" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0" + implementation 'com.google.code.gson:gson:2.10.1' testImplementation "org.jetbrains.kotlin:kotlin-test" } From 9473f0259f125cfeafe4ee94cb7439715f16a7d3 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Mon, 9 Oct 2023 17:44:45 +0200 Subject: [PATCH 04/21] Fix implementation --- packages/patrol/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patrol/android/build.gradle b/packages/patrol/android/build.gradle index e7e0b461c..72e5f853e 100644 --- a/packages/patrol/android/build.gradle +++ b/packages/patrol/android/build.gradle @@ -67,7 +67,7 @@ android { implementation "org.http4k:http4k-core" implementation "com.squareup.okhttp:okhttp:2.7.5" // See https://github.com/square/okhttp/issues/8031 implementation "org.http4k:http4k-server-ktorcio" - implementation 'com.google.code.gson:gson:2.10.1' + implementation "com.google.code.gson:gson:2.10.1" testImplementation "org.jetbrains.kotlin:kotlin-test" } From a39210f6cc15784d130e5983ada882f5668553a1 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Tue, 10 Oct 2023 15:02:18 +0200 Subject: [PATCH 05/21] Downgrade http4k and fix bug related to kotlin_version 1.7.20 --- packages/patrol/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patrol/android/build.gradle b/packages/patrol/android/build.gradle index 72e5f853e..c221bea64 100644 --- a/packages/patrol/android/build.gradle +++ b/packages/patrol/android/build.gradle @@ -63,7 +63,7 @@ android { api "androidx.test.espresso:espresso-core:3.5.0" api "androidx.test.uiautomator:uiautomator:2.2.0" - implementation platform("org.http4k:http4k-bom:5.7.4.0") + implementation platform("org.http4k:http4k-bom:4.48.0.0") implementation "org.http4k:http4k-core" implementation "com.squareup.okhttp:okhttp:2.7.5" // See https://github.com/square/okhttp/issues/8031 implementation "org.http4k:http4k-server-ktorcio" From a659cf41e84862986869e41c764f87e280c4a930 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Tue, 10 Oct 2023 15:30:31 +0200 Subject: [PATCH 06/21] Add comment with explanation --- packages/patrol/android/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/patrol/android/build.gradle b/packages/patrol/android/build.gradle index c221bea64..1a5c07e83 100644 --- a/packages/patrol/android/build.gradle +++ b/packages/patrol/android/build.gradle @@ -63,6 +63,9 @@ android { api "androidx.test.espresso:espresso-core:3.5.0" api "androidx.test.uiautomator:uiautomator:2.2.0" + // We need to downgrade http4k-bom to 4.48.0.0 because apps with kotlin version (ext.kotlin_version) lower than 1.8 + // end up with compile-time errors. We can find more details in a similar problem: + // https://stackoverflow.com/questions/67699823/module-was-compiled-with-an-incompatible-version-of-kotlin-the-binary-version-o implementation platform("org.http4k:http4k-bom:4.48.0.0") implementation "org.http4k:http4k-core" implementation "com.squareup.okhttp:okhttp:2.7.5" // See https://github.com/square/okhttp/issues/8031 From 213873397a050d360529e4a142e1c79eade4c493 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Wed, 11 Oct 2023 00:35:39 +0200 Subject: [PATCH 07/21] fix broken links in the docs --- docs.json | 16 ++++++++-------- docs/{patrol => }/finders/advanced.mdx | 0 docs/{patrol => }/finders/finders-setup.mdx | 0 docs/{patrol => }/finders/overview.mdx | 0 docs/{patrol => }/finders/usage.mdx | 0 docs/{patrol => }/native/advanced.mdx | 0 docs/{patrol => }/native/feature-parity.mdx | 0 docs/{patrol => }/native/overview.mdx | 0 docs/{patrol => }/native/usage.mdx | 0 9 files changed, 8 insertions(+), 8 deletions(-) rename docs/{patrol => }/finders/advanced.mdx (100%) rename docs/{patrol => }/finders/finders-setup.mdx (100%) rename docs/{patrol => }/finders/overview.mdx (100%) rename docs/{patrol => }/finders/usage.mdx (100%) rename docs/{patrol => }/native/advanced.mdx (100%) rename docs/{patrol => }/native/feature-parity.mdx (100%) rename docs/{patrol => }/native/overview.mdx (100%) rename docs/{patrol => }/native/usage.mdx (100%) diff --git a/docs.json b/docs.json index aa6edb8d8..53054f320 100644 --- a/docs.json +++ b/docs.json @@ -20,19 +20,19 @@ [ "Finders", [ - ["Overview", "/patrol/finders/overview"], - ["Usage", "/patrol/finders/usage"], - ["Advanced", "/patrol/finders/advanced"], - ["Using Patrol finders in widget tests", "/patrol/finders/finders-setup"] + ["Overview", "/finders/overview"], + ["Usage", "/finders/usage"], + ["Advanced", "/finders/advanced"], + ["Using Patrol finders in widget tests", "/finders/finders-setup"] ] ], [ "Native automation", [ - ["Overview", "/patrol/native/overview"], - ["Usage", "/patrol/native/usage"], - ["Advanced", "/patrol/native/advanced"], - ["Feature parity", "/patrol/native/feature-parity"] + ["Overview", "/native/overview"], + ["Usage", "/native/usage"], + ["Advanced", "/native/advanced"], + ["Feature parity", "/native/feature-parity"] ] ], [ diff --git a/docs/patrol/finders/advanced.mdx b/docs/finders/advanced.mdx similarity index 100% rename from docs/patrol/finders/advanced.mdx rename to docs/finders/advanced.mdx diff --git a/docs/patrol/finders/finders-setup.mdx b/docs/finders/finders-setup.mdx similarity index 100% rename from docs/patrol/finders/finders-setup.mdx rename to docs/finders/finders-setup.mdx diff --git a/docs/patrol/finders/overview.mdx b/docs/finders/overview.mdx similarity index 100% rename from docs/patrol/finders/overview.mdx rename to docs/finders/overview.mdx diff --git a/docs/patrol/finders/usage.mdx b/docs/finders/usage.mdx similarity index 100% rename from docs/patrol/finders/usage.mdx rename to docs/finders/usage.mdx diff --git a/docs/patrol/native/advanced.mdx b/docs/native/advanced.mdx similarity index 100% rename from docs/patrol/native/advanced.mdx rename to docs/native/advanced.mdx diff --git a/docs/patrol/native/feature-parity.mdx b/docs/native/feature-parity.mdx similarity index 100% rename from docs/patrol/native/feature-parity.mdx rename to docs/native/feature-parity.mdx diff --git a/docs/patrol/native/overview.mdx b/docs/native/overview.mdx similarity index 100% rename from docs/patrol/native/overview.mdx rename to docs/native/overview.mdx diff --git a/docs/patrol/native/usage.mdx b/docs/native/usage.mdx similarity index 100% rename from docs/patrol/native/usage.mdx rename to docs/native/usage.mdx From 8dd2bc600da74842d63652a42ed900a0935e854c Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Wed, 11 Oct 2023 00:38:00 +0200 Subject: [PATCH 08/21] fix remaining broken links --- docs/getting-started.mdx | 4 ++-- docs/index.mdx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index a1a70584c..56caf1167 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -302,8 +302,8 @@ up. To run `integration_test/example_test.dart` on a local Android or iOS device To learn how to write Patrol tests, see [finders] and [native automation] sections. -[native automation]: /patrol/native/usage +[native automation]: /native/usage [finders]: /patrol/finders/usage [Using Patrol finders in widget tests]: /patrol/finders/finders-setup -[Here's why]: /patrol/native/advanced#embrace-the-native-tests +[Here's why]: /native/advanced#embrace-the-native-tests [Patrol CLI]: https://pub.dev/packages/patrol_cli diff --git a/docs/index.mdx b/docs/index.mdx index 43fec1aad..fcdead616 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -39,8 +39,8 @@ or [get Patrol from pub.dev!][patrol_on_pubdev] [![Automated UI testing services][services_graphics]][services] [leancode]: https://leancode.co -[native]: /patrol/native/overview -[finders]: /patrol/finders/overview +[native]: /native/overview +[finders]: /finders/overview [hot restart]: /cli-commands/develop [article_0x]: https://leancode.co/blog/patrol-flutter-first-ui-testing-framework [article_1x]: https://leancode.co/blog/patrol-1-0-powerful-flutter-ui-testing-framework @@ -48,4 +48,4 @@ or [get Patrol from pub.dev!][patrol_on_pubdev] [promo_graphics]: /assets/promo.png [patrol_on_pubdev]: https://pub.dev/packages/patrol [services_graphics]: /assets/services.png -[services]: https://leancode.co/products/automated-ui-testing-in-flutter?utm_source=patrol_page&utm_medium=banner&utm_campaign=service \ No newline at end of file +[services]: https://leancode.co/products/automated-ui-testing-in-flutter?utm_source=patrol_page&utm_medium=banner&utm_campaign=service From d84a3eae972391e472e4310fa228de37843ff707 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Tue, 19 Sep 2023 18:51:45 +0200 Subject: [PATCH 09/21] Add support for ios 11 and 12 --- packages/patrol/example/ios/Podfile | 2 +- packages/patrol/example/ios/Podfile.lock | 4 +- .../Classes/AutomatorServer/Automator.swift | 176 +++++++------- .../AutomatorServer/AutomatorServer.swift | 200 +++++++-------- .../NativeAutomatorServer.swift | 230 ++++++++---------- .../PatrolAppServiceClient.swift | 37 ++- .../AutomatorServer/PatrolServer.swift | 2 +- .../Classes/ObjCPatrolAppServiceClient.swift | 53 ++-- .../ios/Classes/PatrolIntegrationTestRunner.h | 13 +- packages/patrol/ios/patrol.podspec | 2 +- .../ios/ios_telegraph_server_generator.dart | 32 +-- 11 files changed, 374 insertions(+), 377 deletions(-) diff --git a/packages/patrol/example/ios/Podfile b/packages/patrol/example/ios/Podfile index 1a1f646ce..30eedc704 100644 --- a/packages/patrol/example/ios/Podfile +++ b/packages/patrol/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -ios_deployment_target = '13.0' +ios_deployment_target = '11.0' platform :ios, ios_deployment_target # CocoaPods analytics sends network stats synchronously affecting flutter build latency. diff --git a/packages/patrol/example/ios/Podfile.lock b/packages/patrol/example/ios/Podfile.lock index 79b1ff61b..18e033a3c 100644 --- a/packages/patrol/example/ios/Podfile.lock +++ b/packages/patrol/example/ios/Podfile.lock @@ -63,11 +63,11 @@ SPEC CHECKSUMS: geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401 HTTPParserC: aea14c3d2d4ac5beb3988781daa36dfa62e0d9ef integration_test: 13825b8a9334a850581300559b8839134b124670 - patrol: 0966206ac7e717e0ee4d9b6b4e86c8aab62acbfc + patrol: 792c0bb6cc4d552fc8b37f49266341c39e659b4d permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 Telegraph: 12576b119324138e4929792af9e5a1085c2ecbc1 webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a -PODFILE CHECKSUM: 0976da911d4a3d1a369deaf59f82a1c7d01a40d5 +PODFILE CHECKSUM: b2bb71756d032256bcb4043384dd40772d5e6a93 COCOAPODS: 1.12.1 diff --git a/packages/patrol/ios/Classes/AutomatorServer/Automator.swift b/packages/patrol/ios/Classes/AutomatorServer/Automator.swift index 6d01e2fc0..61120ab3d 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/Automator.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/Automator.swift @@ -22,41 +22,41 @@ // MARK: General - func pressHome() async throws { - await runAction("pressing home button") { + func pressHome() throws { + runAction("pressing home button") { self.device.press(XCUIDevice.Button.home) } } - func openApp(_ bundleId: String) async throws { - try await runAction("opening app with id \(bundleId)") { + func openApp(_ bundleId: String) throws { + try runAction("opening app with id \(bundleId)") { let app = try self.getApp(withBundleId: bundleId) app.activate() } } - func openAppSwitcher() async throws { + func openAppSwitcher() throws { // TODO: Implement for iPhones without notch - await runAction("opening app switcher") { + runAction("opening app switcher") { let start = self.springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.999)) let end = self.springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.001)) start.press(forDuration: 0.1, thenDragTo: end) } } - func openControlCenter() async throws { - await runAction("opening control center") { + func openControlCenter() throws { + runAction("opening control center") { self.swipeToOpenControlCenter() } } // MARK: General UI interaction - func tap(onText text: String, inApp bundleId: String, atIndex index: Int) async throws { + func tap(onText text: String, inApp bundleId: String, atIndex index: Int) throws { let view = "view with text \(format: text) at index \(index) in app \(bundleId)" - try await runAction("tapping on \(view)") { + try runAction("tapping on \(view)") { let app = try self.getApp(withBundleId: bundleId) // The below selector is an equivalent of `app.descendants(matching: .any)[text]` @@ -78,8 +78,8 @@ } } - func doubleTap(onText text: String, inApp bundleId: String) async throws { - try await runAction("double tapping on text \(format: text) in app \(bundleId)") { + func doubleTap(onText text: String, inApp bundleId: String) throws { + try runAction("double tapping on text \(format: text) in app \(bundleId)") { let app = try self.getApp(withBundleId: bundleId) let element = app.descendants(matching: .any)[text] @@ -99,7 +99,7 @@ atIndex index: Int, inApp bundleId: String, dismissKeyboard: Bool - ) async throws { + ) throws { var data = data if dismissKeyboard { data = "\(data)\n" @@ -107,7 +107,7 @@ let view = "text field with text \(format: text) at index \(index) in app \(bundleId)" - try await runAction("entering text \(format: data) into \(view)") { + try runAction("entering text \(format: data) into \(view)") { let app = try self.getApp(withBundleId: bundleId) // elementType must be specified as integer @@ -151,7 +151,7 @@ } // Prevent keyboard dismissal from happening too fast - try await Task.sleep(nanoseconds: UInt64(1 * Double(NSEC_PER_SEC))) + sleepTask(timeInSeconds: 1) } func enterText( @@ -159,13 +159,13 @@ byIndex index: Int, inApp bundleId: String, dismissKeyboard: Bool - ) async throws { + ) throws { var data = data if dismissKeyboard { data = "\(data)\n" } - try await runAction("entering text \(format: data) by index \(index) in app \(bundleId)") { + try runAction("entering text \(format: data) by index \(index) in app \(bundleId)") { let app = try self.getApp(withBundleId: bundleId) // elementType must be specified as integer @@ -194,11 +194,11 @@ } // Prevent keyboard dismissal from happening too fast - try await Task.sleep(nanoseconds: UInt64(1 * Double(NSEC_PER_SEC))) + sleepTask(timeInSeconds: 1) } - func waitUntilVisible(onText text: String, inApp bundleId: String) async throws { - try await runAction( + func waitUntilVisible(onText text: String, inApp bundleId: String) throws { + try runAction( "waiting until view with text \(format: text) in app \(bundleId) becomes visible" ) { let app = try self.getApp(withBundleId: bundleId) @@ -213,8 +213,8 @@ // MARK: Services - func enableDarkMode(_ bundleId: String) async throws { - try await runSettingsAction("enabling dark mode", bundleId) { + func enableDarkMode(_ bundleId: String) throws { + try runSettingsAction("enabling dark mode", bundleId) { #if targetEnvironment(simulator) self.preferences.descendants(matching: .any)["Developer"].firstMatch.tap() @@ -231,8 +231,8 @@ } } - func disableDarkMode(_ bundleId: String) async throws { - try await runSettingsAction("disabling dark mode", bundleId) { + func disableDarkMode(_ bundleId: String) throws { + try runSettingsAction("disabling dark mode", bundleId) { #if targetEnvironment(simulator) self.preferences.descendants(matching: .any)["Developer"].firstMatch.tap() @@ -249,8 +249,8 @@ } } - func enableAirplaneMode() async throws { - try await runControlCenterAction("enabling airplane mode") { + func enableAirplaneMode() throws { + try runControlCenterAction("enabling airplane mode") { let toggle = self.springboard.switches["airplane-mode-button"] if toggle.value! as! String == "0" { toggle.tap() @@ -260,8 +260,8 @@ } } - func disableAirplaneMode() async throws { - try await runControlCenterAction("disabling airplane mode") { + func disableAirplaneMode() throws { + try runControlCenterAction("disabling airplane mode") { let toggle = self.springboard.switches["airplane-mode-button"] if toggle.value! as! String == "1" { toggle.tap() @@ -273,8 +273,8 @@ } } - func enableCellular() async throws { - try await runControlCenterAction("enabling cellular") { + func enableCellular() throws { + try runControlCenterAction("enabling cellular") { let toggle = self.springboard.switches["cellular-data-button"] let exists = toggle.waitForExistence(timeout: self.timeout) guard exists else { @@ -289,8 +289,8 @@ } } - func disableCellular() async throws { - try await runControlCenterAction("disabling cellular") { + func disableCellular() throws { + try runControlCenterAction("disabling cellular") { let toggle = self.springboard.switches["cellular-data-button"] let exists = toggle.waitForExistence(timeout: self.timeout) guard exists else { @@ -305,8 +305,8 @@ } } - func enableWiFi() async throws { - try await runControlCenterAction("enabling wifi") { + func enableWiFi() throws { + try runControlCenterAction("enabling wifi") { let toggle = self.springboard.switches["wifi-button"] let exists = toggle.waitForExistence(timeout: self.timeout) guard exists else { @@ -321,8 +321,8 @@ } } - func disableWiFi() async throws { - try await runControlCenterAction("disabling wifi") { + func disableWiFi() throws { + try runControlCenterAction("disabling wifi") { let toggle = self.springboard.switches["wifi-button"] let exists = toggle.waitForExistence(timeout: self.timeout) guard exists else { @@ -337,8 +337,8 @@ } } - func enableBluetooth() async throws { - try await runControlCenterAction("enabling bluetooth") { + func enableBluetooth() throws { + try runControlCenterAction("enabling bluetooth") { let toggle = self.springboard.switches["bluetooth-button"] let exists = toggle.waitForExistence(timeout: self.timeout) guard exists else { @@ -353,8 +353,8 @@ } } - func disableBluetooth() async throws { - try await runControlCenterAction("disabling bluetooth") { + func disableBluetooth() throws { + try runControlCenterAction("disabling bluetooth") { let toggle = self.springboard.switches["bluetooth-button"] let exists = toggle.waitForExistence(timeout: self.timeout) guard exists else { @@ -372,8 +372,8 @@ func getNativeViews( byText text: String, inApp bundleId: String - ) async throws -> [NativeView] { - try await runAction("getting native views matching \(text)") { + ) throws -> [NativeView] { + try runAction("getting native views matching \(text)") { let app = try self.getApp(withBundleId: bundleId) // The below selector is an equivalent of `app.descendants(matching: .any)[text]` @@ -397,31 +397,31 @@ // MARK: Notifications - func openNotifications() async throws { + func openNotifications() throws { // TODO: Check if works on iPhones without notch - await runAction("opening notifications") { + runAction("opening notifications") { let start = self.springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.01)) let end = self.springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.6)) start.press(forDuration: 0.1, thenDragTo: end) } } - func closeNotifications() async throws { + func closeNotifications() throws { // TODO: Check if works on iPhones without notch - await runAction("closing notifications") { + runAction("closing notifications") { let start = self.springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99)) let end = self.springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.6)) start.press(forDuration: 0.1, thenDragTo: end) } } - func closeHeadsUpNotification() async throws { + func closeHeadsUpNotification() throws { // If the notification was triggered just now, let's wait for it - try await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC))) + sleepTask(timeInSeconds: 2) - await runAction("closing heads up notification") { + runAction("closing heads up notification") { let start = self.springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.12)) let end = self.springboard.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.07)) start.press(forDuration: 0.1, thenDragTo: end) @@ -430,12 +430,12 @@ // We can't open notification shade immediately after dismissing // the heads-up notification. Let's wait some reasonable amount of // time for it. - try await Task.sleep(nanoseconds: UInt64(1 * Double(NSEC_PER_SEC))) + sleepTask(timeInSeconds: 1) } - func getNotifications() async throws -> [Notification] { + func getNotifications() throws -> [Notification] { var notifications = [Notification]() - await runAction("getting notifications") { + runAction("getting notifications") { let cells = self.springboard.buttons.matching(identifier: "NotificationCell") .allElementsBoundByIndex for (i, cell) in cells.enumerated() { @@ -448,8 +448,8 @@ return notifications } - func tapOnNotification(byIndex index: Int) async throws { - try await runAction("tapping on notification at index \(index)") { + func tapOnNotification(byIndex index: Int) throws { + try runAction("tapping on notification at index \(index)") { let cells = self.springboard.buttons.matching(identifier: "NotificationCell") .allElementsBoundByIndex guard cells.indices.contains(index) else { @@ -466,8 +466,8 @@ } } - func tapOnNotification(bySubstring substring: String) async throws { - try await runAction("tapping on notification containing text \(format: substring)") { + func tapOnNotification(bySubstring substring: String) throws { + try runAction("tapping on notification containing text \(format: substring)") { let cells = self.springboard.buttons.matching(identifier: "NotificationCell") .allElementsBoundByIndex for (i, cell) in cells.enumerated() { @@ -491,8 +491,8 @@ // MARK: Permissions - func isPermissionDialogVisible(timeout: TimeInterval) async -> Bool { - return await runAction("checking if permission dialog is visible") { + func isPermissionDialogVisible(timeout: TimeInterval) -> Bool { + return runAction("checking if permission dialog is visible") { let systemAlerts = self.springboard.alerts let labels = ["OK", "Allow", "Allow once", "Allow While Using App", "Don’t Allow"] @@ -505,8 +505,8 @@ } } - func allowPermissionWhileUsingApp() async throws { - try await runAction("allowing while using app") { + func allowPermissionWhileUsingApp() throws { + try runAction("allowing while using app") { let systemAlerts = self.springboard.alerts let labels = ["OK", "Allow", "Allow While Using App"] @@ -523,8 +523,8 @@ } } - func allowPermissionOnce() async throws { - try await runAction("allowing once") { + func allowPermissionOnce() throws { + try runAction("allowing once") { let systemAlerts = self.springboard.alerts let labels = ["OK", "Allow", "Allow Once"] @@ -541,8 +541,8 @@ } } - func denyPermission() async throws { - try await runAction("denying permission") { + func denyPermission() throws { + try runAction("denying permission") { let label = "Don’t Allow" // not "Don't Allow"! let systemAlerts = self.springboard.alerts let button = systemAlerts.buttons[label] @@ -556,13 +556,13 @@ } } - func selectFineLocation() async throws { + func selectFineLocation() throws { if iOS13orOlder() { Logger.shared.i("Ignored call to selectFineLocation() (iOS < 14)") return } - try await runAction("selecting fine location") { + try runAction("selecting fine location") { let alerts = self.springboard.alerts let button = alerts.buttons["Precise: Off"] @@ -575,13 +575,13 @@ } } - func selectCoarseLocation() async throws { + func selectCoarseLocation() throws { if iOS13orOlder() { Logger.shared.i("Ignored call to selectCoarseLocation() (iOS < 14)") return } - try await runAction("selecting coarse location") { + try runAction("selecting coarse location") { let alerts = self.springboard.alerts let button = alerts.buttons["Precise: On"] @@ -596,8 +596,8 @@ // MARK: Other - func debug() async throws { - await runAction("debug()") { + func debug() throws { + runAction("debug()") { // TODO: Remove later for i in 0...150 { let element = self.springboard.descendants(matching: .any).element(boundBy: i) @@ -738,14 +738,13 @@ } } - private func runControlCenterAction(_ log: String, block: @escaping () throws -> Void) - async throws + private func runControlCenterAction(_ log: String, block: @escaping () throws -> Void) throws { #if targetEnvironment(simulator) throw PatrolError.internal("Control Center is not available on Simulator") #endif - try await runAction(log) { + try runAction(log) { self.swipeToOpenControlCenter() // perform the action @@ -761,8 +760,8 @@ _ log: String, _ bundleId: String, block: @escaping () -> Void - ) async throws { - try await runAction(log) { + ) throws { + try runAction(log) { self.springboard.activate() self.preferences.launch() // reset to a known state @@ -777,14 +776,25 @@ } } - private func runAction(_ log: String, block: @escaping () throws -> T) async rethrows -> T { - return try await MainActor.run { - Logger.shared.i("\(log)...") - let result = try block() - Logger.shared.i("done \(log)") - Logger.shared.i("result: \(result)") - return result - } + private func runAction(_ log: String, block: @escaping () throws -> T) rethrows -> T { + return try DispatchQueue.main.sync { + Logger.shared.i("\(log)...") + let result = try block() + Logger.shared.i("done \(log)") + Logger.shared.i("result: \(result)") + return result + } + } + + private func sleepTask(timeInSeconds: Double) -> Void { + let group = DispatchGroup() + group.enter() + + DispatchQueue.global().asyncAfter(deadline: .now() + timeInSeconds) { + group.leave() + } + + group.wait() } } diff --git a/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift index 5e800f85d..0f67dea3a 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift @@ -12,47 +12,47 @@ self.onAppReady = onAppReady } - func initialize() async throws {} + func initialize() throws {} - func configure(request: ConfigureRequest) async throws { + func configure(request: ConfigureRequest) throws { automator.configure(timeout: TimeInterval(request.findTimeoutMillis / 1000)) } // MARK: General - func pressHome() async throws { - return try await runCatching { - try await automator.pressHome() + func pressHome() throws { + return try runCatching { + try automator.pressHome() } } - func pressBack() async throws { - return try await runCatching { + func pressBack() throws { + return try runCatching { throw PatrolError.methodNotImplemented("pressBack") } } - func pressRecentApps() async throws { - return try await runCatching { - try await automator.openAppSwitcher() + func pressRecentApps() throws { + return try runCatching { + try automator.openAppSwitcher() } } - func doublePressRecentApps() async throws { - return try await runCatching { + func doublePressRecentApps() throws { + return try runCatching { throw PatrolError.methodNotImplemented("doublePressRecentApps") } } - func openApp(request: OpenAppRequest) async throws { - return try await runCatching { - try await automator.openApp(request.appId) + func openApp(request: OpenAppRequest) throws { + return try runCatching { + try automator.openApp(request.appId) } } - func openQuickSettings(request: OpenQuickSettingsRequest) async throws { - return try await runCatching { - try await automator.openControlCenter() + func openQuickSettings(request: OpenQuickSettingsRequest) throws { + return try runCatching { + try automator.openControlCenter() } } @@ -60,9 +60,9 @@ func getNativeViews( request: GetNativeViewsRequest - ) async throws -> GetNativeViewsResponse { - return try await runCatching { - let nativeViews = try await automator.getNativeViews( + ) throws -> GetNativeViewsResponse { + return try runCatching { + let nativeViews = try automator.getNativeViews( byText: request.selector.text ?? String(), inApp: request.appId ) @@ -71,9 +71,9 @@ } } - func tap(request: TapRequest) async throws { - return try await runCatching { - try await automator.tap( + func tap(request: TapRequest) throws { + return try runCatching { + try automator.tap( onText: request.selector.text ?? String(), inApp: request.appId, atIndex: request.selector.instance ?? 0 @@ -81,26 +81,26 @@ } } - func doubleTap(request: TapRequest) async throws { - return try await runCatching { - try await automator.doubleTap( + func doubleTap(request: TapRequest) throws { + return try runCatching { + try automator.doubleTap( onText: request.selector.text ?? String(), inApp: request.appId ) } } - func enterText(request: EnterTextRequest) async throws { - return try await runCatching { + func enterText(request: EnterTextRequest) throws { + return try runCatching { if let index = request.index { - try await automator.enterText( + try automator.enterText( request.data, byIndex: Int(index), inApp: request.appId, dismissKeyboard: request.keyboardBehavior == .showAndDismiss ) } else if let selector = request.selector { - try await automator.enterText( + try automator.enterText( request.data, byText: selector.text ?? String(), atIndex: selector.instance ?? 0, @@ -113,15 +113,15 @@ } } - func swipe(request: SwipeRequest) async throws { - return try await runCatching { + func swipe(request: SwipeRequest) throws { + return try runCatching { throw PatrolError.methodNotImplemented("swipe") } } - func waitUntilVisible(request: WaitUntilVisibleRequest) async throws { - return try await runCatching { - try await automator.waitUntilVisible( + func waitUntilVisible(request: WaitUntilVisibleRequest) throws { + return try runCatching { + try automator.waitUntilVisible( onText: request.selector.text ?? String(), inApp: request.appId ) @@ -130,103 +130,103 @@ // MARK: Services - func enableAirplaneMode() async throws { - return try await runCatching { - try await automator.enableAirplaneMode() + func enableAirplaneMode() throws { + return try runCatching { + try automator.enableAirplaneMode() } } - func disableAirplaneMode() async throws { - return try await runCatching { - try await automator.disableAirplaneMode() + func disableAirplaneMode() throws { + return try runCatching { + try automator.disableAirplaneMode() } } - func enableWiFi() async throws { - return try await runCatching { - try await automator.enableWiFi() + func enableWiFi() throws { + return try runCatching { + try automator.enableWiFi() } } - func disableWiFi() async throws { - return try await runCatching { - try await automator.disableWiFi() + func disableWiFi() throws { + return try runCatching { + try automator.disableWiFi() } } - func enableCellular() async throws { - return try await runCatching { - try await automator.enableCellular() + func enableCellular() throws { + return try runCatching { + try automator.enableCellular() } } - func disableCellular() async throws { - return try await runCatching { - try await automator.disableCellular() + func disableCellular() throws { + return try runCatching { + try automator.disableCellular() } } - func enableBluetooth() async throws { - return try await runCatching { - try await automator.enableBluetooth() + func enableBluetooth() throws { + return try runCatching { + try automator.enableBluetooth() } } - func disableBluetooth() async throws { - return try await runCatching { - try await automator.disableBluetooth() + func disableBluetooth() throws { + return try runCatching { + try automator.disableBluetooth() } } - func enableDarkMode(request: DarkModeRequest) async throws { - return try await runCatching { - try await automator.enableDarkMode(request.appId) + func enableDarkMode(request: DarkModeRequest) throws { + return try runCatching { + try automator.enableDarkMode(request.appId) } } - func disableDarkMode(request: DarkModeRequest) async throws { - return try await runCatching { - try await automator.disableDarkMode(request.appId) + func disableDarkMode(request: DarkModeRequest) throws { + return try runCatching { + try automator.disableDarkMode(request.appId) } } // MARK: Notifications - func openNotifications() async throws { - return try await runCatching { - try await automator.openNotifications() + func openNotifications() throws { + return try runCatching { + try automator.openNotifications() } } - func closeNotifications() async throws { - return try await runCatching { - try await automator.closeNotifications() + func closeNotifications() throws { + return try runCatching { + try automator.closeNotifications() } } - func closeHeadsUpNotification() async throws { - return try await runCatching { - try await automator.closeHeadsUpNotification() + func closeHeadsUpNotification() throws { + return try runCatching { + try automator.closeHeadsUpNotification() } } func getNotifications( request: GetNotificationsRequest - ) async throws -> GetNotificationsResponse { - return try await runCatching { - let notifications = try await automator.getNotifications() + ) throws -> GetNotificationsResponse { + return try runCatching { + let notifications = try automator.getNotifications() return GetNotificationsResponse(notifications: notifications) } } - func tapOnNotification(request: TapOnNotificationRequest) async throws { - return try await runCatching { + func tapOnNotification(request: TapOnNotificationRequest) throws { + return try runCatching { if let index = request.index { - try await automator.tapOnNotification( + try automator.tapOnNotification( byIndex: index ) } else if let selector = request.selector { - try await automator.tapOnNotification( + try automator.tapOnNotification( bySubstring: selector.textContains ?? String() ) } else { @@ -239,9 +239,9 @@ func isPermissionDialogVisible( request: PermissionDialogVisibleRequest - ) async throws -> PermissionDialogVisibleResponse { - return try await runCatching { - let visible = await automator.isPermissionDialogVisible( + ) throws -> PermissionDialogVisibleResponse { + return try runCatching { + let visible = automator.isPermissionDialogVisible( timeout: TimeInterval(request.timeoutMillis / 1000) ) @@ -250,41 +250,41 @@ } } - func handlePermissionDialog(request: HandlePermissionRequest) async throws { - return try await runCatching { + func handlePermissionDialog(request: HandlePermissionRequest) throws { + return try runCatching { switch request.code { case .whileUsing: - try await automator.allowPermissionWhileUsingApp() + try automator.allowPermissionWhileUsingApp() case .onlyThisTime: - try await automator.allowPermissionOnce() + try automator.allowPermissionOnce() case .denied: - try await automator.denyPermission() + try automator.denyPermission() } } } - func setLocationAccuracy(request: SetLocationAccuracyRequest) async throws { - return try await runCatching { + func setLocationAccuracy(request: SetLocationAccuracyRequest) throws { + return try runCatching { switch request.locationAccuracy { case .coarse: - try await automator.selectCoarseLocation() + try automator.selectCoarseLocation() case .fine: - try await automator.selectFineLocation() + try automator.selectFineLocation() } } } - func debug() async throws { - return try await runCatching { - try await automator.debug() + func debug() throws { + return try runCatching { + try automator.debug() } } - private func runCatching(_ block: () async throws -> T) async throws -> T { + private func runCatching(_ block: () throws -> T) throws -> T { // TODO: Use an interceptor (like on Android) // See: https://github.com/grpc/grpc-swift/issues/1148 do { - return try await block() + return try block() } catch let err as PatrolError { Logger.shared.e(err.description) throw err @@ -293,7 +293,7 @@ } } - func markPatrolAppServiceReady() async throws { + func markPatrolAppServiceReady() throws { onAppReady(true) } } diff --git a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift index 0342b5275..6343bc8a8 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift @@ -8,229 +8,229 @@ import Telegraph protocol NativeAutomatorServer { - func initialize() async throws - func configure(request: ConfigureRequest) async throws - func pressHome() async throws - func pressBack() async throws - func pressRecentApps() async throws - func doublePressRecentApps() async throws - func openApp(request: OpenAppRequest) async throws - func openQuickSettings(request: OpenQuickSettingsRequest) async throws - func getNativeViews(request: GetNativeViewsRequest) async throws -> GetNativeViewsResponse - func tap(request: TapRequest) async throws - func doubleTap(request: TapRequest) async throws - func enterText(request: EnterTextRequest) async throws - func swipe(request: SwipeRequest) async throws - func waitUntilVisible(request: WaitUntilVisibleRequest) async throws - func enableAirplaneMode() async throws - func disableAirplaneMode() async throws - func enableWiFi() async throws - func disableWiFi() async throws - func enableCellular() async throws - func disableCellular() async throws - func enableBluetooth() async throws - func disableBluetooth() async throws - func enableDarkMode(request: DarkModeRequest) async throws - func disableDarkMode(request: DarkModeRequest) async throws - func openNotifications() async throws - func closeNotifications() async throws - func closeHeadsUpNotification() async throws - func getNotifications(request: GetNotificationsRequest) async throws -> GetNotificationsResponse - func tapOnNotification(request: TapOnNotificationRequest) async throws - func isPermissionDialogVisible(request: PermissionDialogVisibleRequest) async throws -> PermissionDialogVisibleResponse - func handlePermissionDialog(request: HandlePermissionRequest) async throws - func setLocationAccuracy(request: SetLocationAccuracyRequest) async throws - func debug() async throws - func markPatrolAppServiceReady() async throws + func initialize() throws + func configure(request: ConfigureRequest) throws + func pressHome() throws + func pressBack() throws + func pressRecentApps() throws + func doublePressRecentApps() throws + func openApp(request: OpenAppRequest) throws + func openQuickSettings(request: OpenQuickSettingsRequest) throws + func getNativeViews(request: GetNativeViewsRequest) throws -> GetNativeViewsResponse + func tap(request: TapRequest) throws + func doubleTap(request: TapRequest) throws + func enterText(request: EnterTextRequest) throws + func swipe(request: SwipeRequest) throws + func waitUntilVisible(request: WaitUntilVisibleRequest) throws + func enableAirplaneMode() throws + func disableAirplaneMode() throws + func enableWiFi() throws + func disableWiFi() throws + func enableCellular() throws + func disableCellular() throws + func enableBluetooth() throws + func disableBluetooth() throws + func enableDarkMode(request: DarkModeRequest) throws + func disableDarkMode(request: DarkModeRequest) throws + func openNotifications() throws + func closeNotifications() throws + func closeHeadsUpNotification() throws + func getNotifications(request: GetNotificationsRequest) throws -> GetNotificationsResponse + func tapOnNotification(request: TapOnNotificationRequest) throws + func isPermissionDialogVisible(request: PermissionDialogVisibleRequest) throws -> PermissionDialogVisibleResponse + func handlePermissionDialog(request: HandlePermissionRequest) throws + func setLocationAccuracy(request: SetLocationAccuracyRequest) throws + func debug() throws + func markPatrolAppServiceReady() throws } extension NativeAutomatorServer { - private func initializeHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await initialize() + private func initializeHandler(request: HTTPRequest) throws -> HTTPResponse { + try initialize() return HTTPResponse(.ok) } - private func configureHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func configureHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(ConfigureRequest.self, from: request.body) - try await configure(request: requestArg) + try configure(request: requestArg) return HTTPResponse(.ok) } - private func pressHomeHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await pressHome() + private func pressHomeHandler(request: HTTPRequest) throws -> HTTPResponse { + try pressHome() return HTTPResponse(.ok) } - private func pressBackHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await pressBack() + private func pressBackHandler(request: HTTPRequest) throws -> HTTPResponse { + try pressBack() return HTTPResponse(.ok) } - private func pressRecentAppsHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await pressRecentApps() + private func pressRecentAppsHandler(request: HTTPRequest) throws -> HTTPResponse { + try pressRecentApps() return HTTPResponse(.ok) } - private func doublePressRecentAppsHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await doublePressRecentApps() + private func doublePressRecentAppsHandler(request: HTTPRequest) throws -> HTTPResponse { + try doublePressRecentApps() return HTTPResponse(.ok) } - private func openAppHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func openAppHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(OpenAppRequest.self, from: request.body) - try await openApp(request: requestArg) + try openApp(request: requestArg) return HTTPResponse(.ok) } - private func openQuickSettingsHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func openQuickSettingsHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(OpenQuickSettingsRequest.self, from: request.body) - try await openQuickSettings(request: requestArg) + try openQuickSettings(request: requestArg) return HTTPResponse(.ok) } - private func getNativeViewsHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func getNativeViewsHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(GetNativeViewsRequest.self, from: request.body) - let response = try await getNativeViews(request: requestArg) + let response = try getNativeViews(request: requestArg) let body = try JSONEncoder().encode(response) return HTTPResponse(.ok, body: body) } - private func tapHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func tapHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(TapRequest.self, from: request.body) - try await tap(request: requestArg) + try tap(request: requestArg) return HTTPResponse(.ok) } - private func doubleTapHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func doubleTapHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(TapRequest.self, from: request.body) - try await doubleTap(request: requestArg) + try doubleTap(request: requestArg) return HTTPResponse(.ok) } - private func enterTextHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func enterTextHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(EnterTextRequest.self, from: request.body) - try await enterText(request: requestArg) + try enterText(request: requestArg) return HTTPResponse(.ok) } - private func swipeHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func swipeHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(SwipeRequest.self, from: request.body) - try await swipe(request: requestArg) + try swipe(request: requestArg) return HTTPResponse(.ok) } - private func waitUntilVisibleHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func waitUntilVisibleHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(WaitUntilVisibleRequest.self, from: request.body) - try await waitUntilVisible(request: requestArg) + try waitUntilVisible(request: requestArg) return HTTPResponse(.ok) } - private func enableAirplaneModeHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await enableAirplaneMode() + private func enableAirplaneModeHandler(request: HTTPRequest) throws -> HTTPResponse { + try enableAirplaneMode() return HTTPResponse(.ok) } - private func disableAirplaneModeHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await disableAirplaneMode() + private func disableAirplaneModeHandler(request: HTTPRequest) throws -> HTTPResponse { + try disableAirplaneMode() return HTTPResponse(.ok) } - private func enableWiFiHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await enableWiFi() + private func enableWiFiHandler(request: HTTPRequest) throws -> HTTPResponse { + try enableWiFi() return HTTPResponse(.ok) } - private func disableWiFiHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await disableWiFi() + private func disableWiFiHandler(request: HTTPRequest) throws -> HTTPResponse { + try disableWiFi() return HTTPResponse(.ok) } - private func enableCellularHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await enableCellular() + private func enableCellularHandler(request: HTTPRequest) throws -> HTTPResponse { + try enableCellular() return HTTPResponse(.ok) } - private func disableCellularHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await disableCellular() + private func disableCellularHandler(request: HTTPRequest) throws -> HTTPResponse { + try disableCellular() return HTTPResponse(.ok) } - private func enableBluetoothHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await enableBluetooth() + private func enableBluetoothHandler(request: HTTPRequest) throws -> HTTPResponse { + try enableBluetooth() return HTTPResponse(.ok) } - private func disableBluetoothHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await disableBluetooth() + private func disableBluetoothHandler(request: HTTPRequest) throws -> HTTPResponse { + try disableBluetooth() return HTTPResponse(.ok) } - private func enableDarkModeHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func enableDarkModeHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(DarkModeRequest.self, from: request.body) - try await enableDarkMode(request: requestArg) + try enableDarkMode(request: requestArg) return HTTPResponse(.ok) } - private func disableDarkModeHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func disableDarkModeHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(DarkModeRequest.self, from: request.body) - try await disableDarkMode(request: requestArg) + try disableDarkMode(request: requestArg) return HTTPResponse(.ok) } - private func openNotificationsHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await openNotifications() + private func openNotificationsHandler(request: HTTPRequest) throws -> HTTPResponse { + try openNotifications() return HTTPResponse(.ok) } - private func closeNotificationsHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await closeNotifications() + private func closeNotificationsHandler(request: HTTPRequest) throws -> HTTPResponse { + try closeNotifications() return HTTPResponse(.ok) } - private func closeHeadsUpNotificationHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await closeHeadsUpNotification() + private func closeHeadsUpNotificationHandler(request: HTTPRequest) throws -> HTTPResponse { + try closeHeadsUpNotification() return HTTPResponse(.ok) } - private func getNotificationsHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func getNotificationsHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(GetNotificationsRequest.self, from: request.body) - let response = try await getNotifications(request: requestArg) + let response = try getNotifications(request: requestArg) let body = try JSONEncoder().encode(response) return HTTPResponse(.ok, body: body) } - private func tapOnNotificationHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func tapOnNotificationHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(TapOnNotificationRequest.self, from: request.body) - try await tapOnNotification(request: requestArg) + try tapOnNotification(request: requestArg) return HTTPResponse(.ok) } - private func isPermissionDialogVisibleHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func isPermissionDialogVisibleHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(PermissionDialogVisibleRequest.self, from: request.body) - let response = try await isPermissionDialogVisible(request: requestArg) + let response = try isPermissionDialogVisible(request: requestArg) let body = try JSONEncoder().encode(response) return HTTPResponse(.ok, body: body) } - private func handlePermissionDialogHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func handlePermissionDialogHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(HandlePermissionRequest.self, from: request.body) - try await handlePermissionDialog(request: requestArg) + try handlePermissionDialog(request: requestArg) return HTTPResponse(.ok) } - private func setLocationAccuracyHandler(request: HTTPRequest) async throws -> HTTPResponse { + private func setLocationAccuracyHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(SetLocationAccuracyRequest.self, from: request.body) - try await setLocationAccuracy(request: requestArg) + try setLocationAccuracy(request: requestArg) return HTTPResponse(.ok) } - private func debugHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await debug() + private func debugHandler(request: HTTPRequest) throws -> HTTPResponse { + try debug() return HTTPResponse(.ok) } - private func markPatrolAppServiceReadyHandler(request: HTTPRequest) async throws -> HTTPResponse { - try await markPatrolAppServiceReady() + private func markPatrolAppServiceReadyHandler(request: HTTPRequest) throws -> HTTPResponse { + try markPatrolAppServiceReady() return HTTPResponse(.ok) } } @@ -410,34 +410,14 @@ extension NativeAutomatorServer { } } -fileprivate class Box { - var result: Result? = nil -} extension NativeAutomatorServer { - private func handleRequest(request: HTTPRequest, handler: @escaping (HTTPRequest) async throws -> HTTPResponse) -> HTTPResponse { + private func handleRequest(request: HTTPRequest, handler: @escaping (HTTPRequest) throws -> HTTPResponse) -> HTTPResponse { do { - return try unsafeWait { - return try await handler(request) - } + return try handler(request) } catch let err { return HTTPResponse(.badRequest, headers: [:], error: err) } } - - private func unsafeWait(_ f: @escaping () async throws -> ResultType) throws -> ResultType { - let box = Box() - let sema = DispatchSemaphore(value: 0) - Task { - do { - let val = try await f() - box.result = .success(val) - } catch { - box.result = .failure(error) - } - sema.signal() - } - sema.wait() - return try box.result!.get() - } } + diff --git a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift index b3ed8d553..7fd9994c5 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift @@ -16,16 +16,20 @@ class PatrolAppServiceClient { self.timeout = timeout } - func listDartTests() async throws -> ListDartTestsResponse { - return try await performRequest(requestName: "listDartTests") + func listDartTests(completion: @escaping (Result) -> Void) { + performRequest(requestName: "listDartTests", completion: completion) } - func runDartTest(request: RunDartTestRequest) async throws -> RunDartTestResponse { - let body = try JSONEncoder().encode(request) - return try await performRequest(requestName: "runDartTest", body: body) + func runDartTest(request: RunDartTestRequest, completion: @escaping (Result) -> Void) { + do { + let body = try JSONEncoder().encode(request) + performRequest(requestName: "runDartTest", body: body, completion: completion) + } catch let err { + completion(.failure(err)) + } } - private func performRequest(requestName: String, body: Data? = nil) async throws -> TResult { + private func performRequest(requestName: String, body: Data? = nil, completion: @escaping (Result) -> Void) { let url = URL(string: "http://\(address):\(port)/\(requestName)")! let urlconfig = URLSessionConfiguration.default @@ -37,11 +41,20 @@ class PatrolAppServiceClient { request.httpBody = body request.timeoutInterval = timeout - let (data, response) = try await URLSession(configuration: urlconfig).data(for: request) - guard (response as? HTTPURLResponse)?.statusCode == 200 else { - throw PatrolError.internal("Invalid response: \(response) \(data)") - } - - return try JSONDecoder().decode(TResult.self, from: data) + let session = URLSession(configuration: urlconfig) + + session.dataTask(with: request) { data, response, error in + if (response as? HTTPURLResponse)?.statusCode == 200 { + do { + let object = try JSONDecoder().decode(TResult.self, from: data!) + completion(.success(object)) + } catch let err { + completion(.failure(err)) + } + } else { + let message = "Invalid response: \(String(describing: response)) \(String(describing: data))" + completion(.failure(PatrolError.internal(message))) + } + }.resume() } } diff --git a/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift b/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift index ef2575e5a..444fccfca 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift @@ -44,7 +44,7 @@ import Telegraph #endif } - @objc public func start() async throws { + @objc public func start() throws -> Void { #if PATROL_ENABLED Logger.shared.i("Starting server...") diff --git a/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift b/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift index d7626cadb..1b48ef1da 100644 --- a/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift +++ b/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift @@ -29,31 +29,44 @@ client = PatrolAppServiceClient(port: port, address: "localhost", timeout: timeout) } - @objc public func listDartTests() async throws -> [String] { - NSLog("PatrolAppServiceClient.listDartTests()") - - let response = try await client.listDartTests() - - return response.group.listTestsFlat(parentGroupName: "").map { - $0.name + @objc public func listDartTests(completion: @escaping ([String]?, Error?) -> Void) { + NSLog("PatrolAppServiceClient.listDartTests()") + + client.listDartTests() { result in + switch result { + case .success(let result): + let output = result.group.listTestsFlat(parentGroupName: "").map { + $0.name + } + completion(output, nil) + case .failure(let error): + completion(nil, error) + } + } } - } - @objc public func runDartTest(name: String) async throws -> ObjCRunDartTestResponse { + @objc public func runDartTest(name: String, completion: @escaping (ObjCRunDartTestResponse?, Error?) -> Void) { // TODO: simple workaround - patrolAppService starts running too slowly. // We should wait for appReady in the dynamically created test case method, // before calling runDartTest() (in PATROL_INTEGRATION_TEST_IOS_MACRO) - try await Task.sleep(nanoseconds: UInt64(1 * Double(NSEC_PER_SEC))) - - NSLog("PatrolAppServiceClient.runDartTest(\(name))") - - let request = RunDartTestRequest(name: name) - let result = try await client.runDartTest(request: request) - - return ObjCRunDartTestResponse( - passed: result.result == .success, - details: result.details - ) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + NSLog("PatrolAppServiceClient.runDartTest(\(name))") + + let request = RunDartTestRequest(name: name) + self.client.runDartTest(request: request) { result in + switch result { + case .success(let result): + let testRespone = ObjCRunDartTestResponse( + passed: result.result == .success, + details: result.details + ) + completion(testRespone, nil) + case .failure(let error): + completion(nil, error) + } + + } + } } } diff --git a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h index 3d7fe82c8..be2c79d38 100644 --- a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h +++ b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h @@ -16,11 +16,12 @@ @implementation __test_class \ \ +(NSArray *)testInvocations { \ - /* Start native automation gRPC server */ \ + /* Start native automation server */ \ PatrolServer *server = [[PatrolServer alloc] init]; \ - [server startWithCompletionHandler:^(NSError * err) { \ - NSLog(@"Server loop done, error: %@", err); \ - }]; \ + \ + NSError * _Nullable __autoreleasing * _Nullable err = NULL; \ + [server startAndReturnError: err]; \ + /* TODO handle error */ \ \ /* Create a client for PatrolAppService, which lets us list and run Dart tests */ \ __block ObjCPatrolAppServiceClient *appServiceClient = [[ObjCPatrolAppServiceClient alloc] init]; \ @@ -42,7 +43,7 @@ \ __block NSArray *dartTests = NULL; \ [appServiceClient \ - listDartTestsWithCompletionHandler:^(NSArray *_Nullable tests, NSError *_Nullable err) { \ + listDartTestsWithCompletion:^(NSArray *_Nullable tests, NSError *_Nullable err) { \ if (err != NULL) { \ NSLog(@"listDartTests(): failed, err: %@", err); \ } \ @@ -75,7 +76,7 @@ \ __block ObjCRunDartTestResponse *response = NULL; \ [appServiceClient runDartTestWithName:dartTest \ - completionHandler:^(ObjCRunDartTestResponse *_Nullable r, NSError *_Nullable err) { \ + completion:^(ObjCRunDartTestResponse *_Nullable r, NSError *_Nullable err) { \ if (err != NULL) { \ NSLog(@"runDartTestWithName(%@): failed, err: %@", dartTest, err); \ } \ diff --git a/packages/patrol/ios/patrol.podspec b/packages/patrol/ios/patrol.podspec index 15af13f7c..f07224d5d 100644 --- a/packages/patrol/ios/patrol.podspec +++ b/packages/patrol/ios/patrol.podspec @@ -18,7 +18,7 @@ Runs tests that use flutter_test and patrol APIs as native iOS integration tests s.dependency 'Flutter' s.weak_framework = 'XCTest' s.ios.framework = 'UIKit' - s.platform = :ios, '13.0' + s.platform = :ios, '11.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/packages/patrol_gen/lib/src/generators/ios/ios_telegraph_server_generator.dart b/packages/patrol_gen/lib/src/generators/ios/ios_telegraph_server_generator.dart index a51ef770b..a1380524c 100644 --- a/packages/patrol_gen/lib/src/generators/ios/ios_telegraph_server_generator.dart +++ b/packages/patrol_gen/lib/src/generators/ios/ios_telegraph_server_generator.dart @@ -49,7 +49,7 @@ $endpoints endpoint.request != null ? 'request: ${endpoint.request!.name}' : ''; final response = endpoint.response != null ? ' -> ${endpoint.response!.name}' : ''; - return ' func ${endpoint.name}($request) async throws$response'; + return ' func ${endpoint.name}($request) throws$response'; } String _generateHandlers(Service service) { @@ -76,8 +76,8 @@ $handlers : ' return HTTPResponse(.ok)'; return ''' - private func ${endpoint.name}Handler(request: HTTPRequest) async throws -> HTTPResponse {$requestArg - ${responseVariable}try await ${endpoint.name}(${requestArg.isNotEmpty ? 'request: requestArg' : ''}) + private func ${endpoint.name}Handler(request: HTTPRequest) throws -> HTTPResponse {$requestArg + ${responseVariable}try ${endpoint.name}(${requestArg.isNotEmpty ? 'request: requestArg' : ''}) $response }'''; } @@ -106,37 +106,17 @@ $routes String _generateUtils() { // https://forums.swift.org/t/using-async-functions-from-synchronous-functions-and-breaking-all-the-rules/59782 return ''' -fileprivate class Box { - var result: Result? = nil -} extension NativeAutomatorServer { - private func handleRequest(request: HTTPRequest, handler: @escaping (HTTPRequest) async throws -> HTTPResponse) -> HTTPResponse { + private func handleRequest(request: HTTPRequest, handler: @escaping (HTTPRequest) throws -> HTTPResponse) -> HTTPResponse { do { - return try unsafeWait { - return try await handler(request) - } + return try handler(request) } catch let err { return HTTPResponse(.badRequest, headers: [:], error: err) } } - - private func unsafeWait(_ f: @escaping () async throws -> ResultType) throws -> ResultType { - let box = Box() - let sema = DispatchSemaphore(value: 0) - Task { - do { - let val = try await f() - box.result = .success(val) - } catch { - box.result = .failure(error) - } - sema.signal() - } - sema.wait() - return try box.result!.get() - } } + '''; } } From d4db2d77e1e93f82272955da69c581dea50f8c46 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Wed, 20 Sep 2023 14:44:22 +0200 Subject: [PATCH 10/21] Fix swift-format issues --- .../Classes/AutomatorServer/Automator.swift | 35 +++++----- .../PatrolAppServiceClient.swift | 41 ++++++------ .../AutomatorServer/PatrolServer.swift | 2 +- .../Classes/ObjCPatrolAppServiceClient.swift | 60 ++++++++--------- .../ios/ios_url_session_client_generator.dart | 64 +++++++++++++------ 5 files changed, 117 insertions(+), 85 deletions(-) diff --git a/packages/patrol/ios/Classes/AutomatorServer/Automator.swift b/packages/patrol/ios/Classes/AutomatorServer/Automator.swift index 61120ab3d..741d034c6 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/Automator.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/Automator.swift @@ -738,8 +738,7 @@ } } - private func runControlCenterAction(_ log: String, block: @escaping () throws -> Void) throws - { + private func runControlCenterAction(_ log: String, block: @escaping () throws -> Void) throws { #if targetEnvironment(simulator) throw PatrolError.internal("Control Center is not available on Simulator") #endif @@ -777,24 +776,24 @@ } private func runAction(_ log: String, block: @escaping () throws -> T) rethrows -> T { - return try DispatchQueue.main.sync { - Logger.shared.i("\(log)...") - let result = try block() - Logger.shared.i("done \(log)") - Logger.shared.i("result: \(result)") - return result - } + return try DispatchQueue.main.sync { + Logger.shared.i("\(log)...") + let result = try block() + Logger.shared.i("done \(log)") + Logger.shared.i("result: \(result)") + return result + } } - - private func sleepTask(timeInSeconds: Double) -> Void { - let group = DispatchGroup() - group.enter() - - DispatchQueue.global().asyncAfter(deadline: .now() + timeInSeconds) { - group.leave() - } - group.wait() + private func sleepTask(timeInSeconds: Double) { + let group = DispatchGroup() + group.enter() + + DispatchQueue.global().asyncAfter(deadline: .now() + timeInSeconds) { + group.leave() + } + + group.wait() } } diff --git a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift index 7fd9994c5..f6da06cbe 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift @@ -17,19 +17,21 @@ class PatrolAppServiceClient { } func listDartTests(completion: @escaping (Result) -> Void) { - performRequest(requestName: "listDartTests", completion: completion) + performRequest(requestName: "listDartTests", completion: completion) } func runDartTest(request: RunDartTestRequest, completion: @escaping (Result) -> Void) { - do { - let body = try JSONEncoder().encode(request) - performRequest(requestName: "runDartTest", body: body, completion: completion) - } catch let err { - completion(.failure(err)) - } + do { + let body = try JSONEncoder().encode(request) + performRequest(requestName: "runDartTest", body: body, completion: completion) + } catch let err { + completion(.failure(err)) + } } - private func performRequest(requestName: String, body: Data? = nil, completion: @escaping (Result) -> Void) { + private func performRequest( + requestName: String, body: Data? = nil, completion: @escaping (Result) -> Void + ) { let url = URL(string: "http://\(address):\(port)/\(requestName)")! let urlconfig = URLSessionConfiguration.default @@ -42,19 +44,20 @@ class PatrolAppServiceClient { request.timeoutInterval = timeout let session = URLSession(configuration: urlconfig) - + session.dataTask(with: request) { data, response, error in - if (response as? HTTPURLResponse)?.statusCode == 200 { - do { - let object = try JSONDecoder().decode(TResult.self, from: data!) - completion(.success(object)) - } catch let err { - completion(.failure(err)) - } - } else { - let message = "Invalid response: \(String(describing: response)) \(String(describing: data))" - completion(.failure(PatrolError.internal(message))) + if (response as? HTTPURLResponse)?.statusCode == 200 { + do { + let object = try JSONDecoder().decode(TResult.self, from: data!) + completion(.success(object)) + } catch let err { + completion(.failure(err)) } + } else { + let message = + "Invalid response: \(String(describing: response)) \(String(describing: data))" + completion(.failure(PatrolError.internal(message))) + } }.resume() } } diff --git a/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift b/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift index 444fccfca..9c9b28d61 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift @@ -44,7 +44,7 @@ import Telegraph #endif } - @objc public func start() throws -> Void { + @objc public func start() throws { #if PATROL_ENABLED Logger.shared.i("Starting server...") diff --git a/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift b/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift index 1b48ef1da..9b2b3290a 100644 --- a/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift +++ b/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift @@ -29,43 +29,45 @@ client = PatrolAppServiceClient(port: port, address: "localhost", timeout: timeout) } - @objc public func listDartTests(completion: @escaping ([String]?, Error?) -> Void) { - NSLog("PatrolAppServiceClient.listDartTests()") - - client.listDartTests() { result in - switch result { - case .success(let result): - let output = result.group.listTestsFlat(parentGroupName: "").map { - $0.name - } - completion(output, nil) - case .failure(let error): - completion(nil, error) - } + @objc public func listDartTests(completion: @escaping ([String]?, Error?) -> Void) { + NSLog("PatrolAppServiceClient.listDartTests()") + + client.listDartTests { result in + switch result { + case .success(let result): + let output = result.group.listTestsFlat(parentGroupName: "").map { + $0.name } + completion(output, nil) + case .failure(let error): + completion(nil, error) + } } + } - @objc public func runDartTest(name: String, completion: @escaping (ObjCRunDartTestResponse?, Error?) -> Void) { + @objc public func runDartTest( + name: String, completion: @escaping (ObjCRunDartTestResponse?, Error?) -> Void + ) { // TODO: simple workaround - patrolAppService starts running too slowly. // We should wait for appReady in the dynamically created test case method, // before calling runDartTest() (in PATROL_INTEGRATION_TEST_IOS_MACRO) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - NSLog("PatrolAppServiceClient.runDartTest(\(name))") - - let request = RunDartTestRequest(name: name) - self.client.runDartTest(request: request) { result in - switch result { - case .success(let result): - let testRespone = ObjCRunDartTestResponse( - passed: result.result == .success, - details: result.details - ) - completion(testRespone, nil) - case .failure(let error): - completion(nil, error) - } - + NSLog("PatrolAppServiceClient.runDartTest(\(name))") + + let request = RunDartTestRequest(name: name) + self.client.runDartTest(request: request) { result in + switch result { + case .success(let result): + let testRespone = ObjCRunDartTestResponse( + passed: result.result == .success, + details: result.details + ) + completion(testRespone, nil) + case .failure(let error): + completion(nil, error) } + + } } } } diff --git a/packages/patrol_gen/lib/src/generators/ios/ios_url_session_client_generator.dart b/packages/patrol_gen/lib/src/generators/ios/ios_url_session_client_generator.dart index 7d0c8cabb..037b5ee18 100644 --- a/packages/patrol_gen/lib/src/generators/ios/ios_url_session_client_generator.dart +++ b/packages/patrol_gen/lib/src/generators/ios/ios_url_session_client_generator.dart @@ -31,8 +31,8 @@ class IOSURLSessionClientGenerator { const url = r'http://\(address):\(port)/\(requestName)'; - const throwException = - r'throw PatrolError.internal("Invalid response: \(response) \(data)")'; + const exceptionMessage = + r'Invalid response: \(String(describing: response)) \(String(describing: data))'; return ''' class ${service.name}Client { @@ -48,7 +48,9 @@ class ${service.name}Client { $endpoints - private func performRequest(requestName: String, body: Data? = nil) async throws -> TResult { + private func performRequest( + requestName: String, body: Data? = nil, completion: @escaping (Result) -> Void + ) { let url = URL(string: "$url")! let urlconfig = URLSessionConfiguration.default @@ -60,32 +62,58 @@ $endpoints request.httpBody = body request.timeoutInterval = timeout - let (data, response) = try await URLSession(configuration: urlconfig).data(for: request) - guard (response as? HTTPURLResponse)?.statusCode == 200 else { - $throwException - } - - return try JSONDecoder().decode(TResult.self, from: data) + let session = URLSession(configuration: urlconfig) + + session.dataTask(with: request) { data, response, error in + if (response as? HTTPURLResponse)?.statusCode == 200 { + do { + let object = try JSONDecoder().decode(TResult.self, from: data!) + completion(.success(object)) + } catch let err { + completion(.failure(err)) + } + } else { + let message = + "$exceptionMessage" + completion(.failure(PatrolError.internal(message))) + } + }.resume() } } '''; } String _createEndpoint(Endpoint endpoint) { - final parameterDef = + final requestDef = endpoint.request != null ? 'request: ${endpoint.request!.name}' : ''; - final returnDef = - endpoint.response != null ? '-> ${endpoint.response!.name}' : ''; - final bodyCode = endpoint.request != null - ? '\n let body = try JSONEncoder().encode(request)' - : ''; + final completionDef = endpoint.response != null + ? 'completion: @escaping (Result<${endpoint.response!.name}, Error>) -> Void' + : 'completion: @escaping (Result) -> Void'; + + final parameters = endpoint.request != null + ? '$requestDef, $completionDef' + : completionDef; - final bodyArgument = bodyCode.isNotEmpty ? ', body: body' : ''; + final arguments = [ + 'requestName: "${endpoint.name}"', + if (endpoint.request != null) 'body: body', + 'completion: completion' + ].join(', '); + + final bodyCode = endpoint.request != null + ? ''' + do { + let body = try JSONEncoder().encode(request) + performRequest($arguments) + } catch let err { + completion(.failure(err)) + }''' + : ' performRequest($arguments)'; return ''' - func ${endpoint.name}($parameterDef) async throws $returnDef {$bodyCode - return try await performRequest(requestName: "${endpoint.name}"$bodyArgument) + func ${endpoint.name}($parameters) { +$bodyCode }'''; } } From 7605a41f0daf2851377e1cc0d75b97a41951f407 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Wed, 20 Sep 2023 14:51:22 +0200 Subject: [PATCH 11/21] Log patrolServer.start() failed --- packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h index be2c79d38..898eb6786 100644 --- a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h +++ b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h @@ -21,7 +21,9 @@ \ NSError * _Nullable __autoreleasing * _Nullable err = NULL; \ [server startAndReturnError: err]; \ - /* TODO handle error */ \ + if (err != NULL) { \ + NSLog(@"patrolServer.start(): failed, err: %@", err); \ + } \ \ /* Create a client for PatrolAppService, which lets us list and run Dart tests */ \ __block ObjCPatrolAppServiceClient *appServiceClient = [[ObjCPatrolAppServiceClient alloc] init]; \ From 3128ac749d10bb2f86551f1a7d592cea3f2fdfb4 Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Wed, 20 Sep 2023 15:09:25 +0200 Subject: [PATCH 12/21] Fix clang-format --- .../ios/Classes/PatrolIntegrationTestRunner.h | 203 +++++++++--------- 1 file changed, 101 insertions(+), 102 deletions(-) diff --git a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h index 898eb6786..8483bf71c 100644 --- a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h +++ b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h @@ -9,105 +9,104 @@ // For every Flutter dart test, dynamically generate an Objective-C method mirroring the test results // so it is reported as a native XCTest run result. -#define PATROL_INTEGRATION_TEST_IOS_RUNNER(__test_class) \ - @interface __test_class : XCTestCase \ - @end \ - \ - @implementation __test_class \ - \ - +(NSArray *)testInvocations { \ - /* Start native automation server */ \ - PatrolServer *server = [[PatrolServer alloc] init]; \ - \ - NSError * _Nullable __autoreleasing * _Nullable err = NULL; \ - [server startAndReturnError: err]; \ - if (err != NULL) { \ - NSLog(@"patrolServer.start(): failed, err: %@", err); \ - } \ - \ - /* Create a client for PatrolAppService, which lets us list and run Dart tests */ \ - __block ObjCPatrolAppServiceClient *appServiceClient = [[ObjCPatrolAppServiceClient alloc] init]; \ - \ - /* Allow the Local Network permission required by Dart Observatory */ \ - XCUIApplication *springboard = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; \ - XCUIElementQuery *systemAlerts = springboard.alerts; \ - if (systemAlerts.buttons[@"Allow"].exists) { \ - [systemAlerts.buttons[@"Allow"] tap]; \ - } \ - \ - /* Run the app for the first time to gather Dart tests */ \ - [[[XCUIApplication alloc] init] launch]; \ - \ - /* Spin the runloop waiting until the app reports that it is ready to report Dart tests */ \ - while (!server.appReady) { \ - [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ - } \ - \ - __block NSArray *dartTests = NULL; \ - [appServiceClient \ - listDartTestsWithCompletion:^(NSArray *_Nullable tests, NSError *_Nullable err) { \ - if (err != NULL) { \ - NSLog(@"listDartTests(): failed, err: %@", err); \ - } \ - \ - dartTests = tests; \ - }]; \ - \ - /* Spin the runloop waiting until the app reports the Dart tests it contains */ \ - while (!dartTests) { \ - [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ - } \ - \ - NSLog(@"Got %lu Dart tests: %@", dartTests.count, dartTests); \ - \ - NSMutableArray *invocations = [[NSMutableArray alloc] init]; \ - \ - /** \ - * Once Dart tests are available, we: \ - * \ - * Step 1. Dynamically add test case methods that request execution of an individual Dart test file. \ - * \ - * Step 2. Create invocations to the generated methods and return them \ - */ \ - \ - for (NSString * dartTest in dartTests) { \ - /* Step 1 - dynamically create test cases */ \ - \ - IMP implementation = imp_implementationWithBlock(^(id _self) { \ - [[[XCUIApplication alloc] init] launch]; \ - \ - __block ObjCRunDartTestResponse *response = NULL; \ - [appServiceClient runDartTestWithName:dartTest \ - completion:^(ObjCRunDartTestResponse *_Nullable r, NSError *_Nullable err) { \ - if (err != NULL) { \ - NSLog(@"runDartTestWithName(%@): failed, err: %@", dartTest, err); \ - } \ - \ - response = r; \ - }]; \ - \ - /* Wait until Dart test finishes */ \ - while (!response) { \ - [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ - } \ - \ - XCTAssertTrue(response.passed, @"%@", response.details); \ - }); \ - NSString *selectorName = [PatrolUtils createMethodNameFromPatrolGeneratedGroup:dartTest]; \ - SEL selector = NSSelectorFromString(selectorName); \ - class_addMethod(self, selector, implementation, "v@:"); \ - \ - /* Step 2 – create invocations to the dynamically created methods */ \ - NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector]; \ - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; \ - invocation.selector = selector; \ - \ - NSLog(@"RunnerUITests.testInvocations(): selectorName = %@, signature: %@", selectorName, signature); \ - \ - [invocations addObject:invocation]; \ - } \ - \ - return invocations; \ - } \ - \ - @end\ +#define PATROL_INTEGRATION_TEST_IOS_RUNNER(__test_class) \ + @interface __test_class : XCTestCase \ + @end \ + \ + @implementation __test_class \ + \ + +(NSArray *)testInvocations { \ + /* Start native automation server */ \ + PatrolServer *server = [[PatrolServer alloc] init]; \ + \ + NSError *_Nullable __autoreleasing *_Nullable err = NULL; \ + [server startAndReturnError:err]; \ + if (err != NULL) { \ + NSLog(@"patrolServer.start(): failed, err: %@", err); \ + } \ + \ + /* Create a client for PatrolAppService, which lets us list and run Dart tests */ \ + __block ObjCPatrolAppServiceClient *appServiceClient = [[ObjCPatrolAppServiceClient alloc] init]; \ + \ + /* Allow the Local Network permission required by Dart Observatory */ \ + XCUIApplication *springboard = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; \ + XCUIElementQuery *systemAlerts = springboard.alerts; \ + if (systemAlerts.buttons[@"Allow"].exists) { \ + [systemAlerts.buttons[@"Allow"] tap]; \ + } \ + \ + /* Run the app for the first time to gather Dart tests */ \ + [[[XCUIApplication alloc] init] launch]; \ + \ + /* Spin the runloop waiting until the app reports that it is ready to report Dart tests */ \ + while (!server.appReady) { \ + [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ + } \ + \ + __block NSArray *dartTests = NULL; \ + [appServiceClient listDartTestsWithCompletion:^(NSArray *_Nullable tests, NSError *_Nullable err) { \ + if (err != NULL) { \ + NSLog(@"listDartTests(): failed, err: %@", err); \ + } \ + \ + dartTests = tests; \ + }]; \ + \ + /* Spin the runloop waiting until the app reports the Dart tests it contains */ \ + while (!dartTests) { \ + [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ + } \ + \ + NSLog(@"Got %lu Dart tests: %@", dartTests.count, dartTests); \ + \ + NSMutableArray *invocations = [[NSMutableArray alloc] init]; \ + \ + /** \ + * Once Dart tests are available, we: \ + * \ + * Step 1. Dynamically add test case methods that request execution of an individual Dart test file. \ + * \ + * Step 2. Create invocations to the generated methods and return them \ + */ \ + \ + for (NSString * dartTest in dartTests) { \ + /* Step 1 - dynamically create test cases */ \ + \ + IMP implementation = imp_implementationWithBlock(^(id _self) { \ + [[[XCUIApplication alloc] init] launch]; \ + \ + __block ObjCRunDartTestResponse *response = NULL; \ + [appServiceClient runDartTestWithName:dartTest \ + completion:^(ObjCRunDartTestResponse *_Nullable r, NSError *_Nullable err) { \ + if (err != NULL) { \ + NSLog(@"runDartTestWithName(%@): failed, err: %@", dartTest, err); \ + } \ + \ + response = r; \ + }]; \ + \ + /* Wait until Dart test finishes */ \ + while (!response) { \ + [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \ + } \ + \ + XCTAssertTrue(response.passed, @"%@", response.details); \ + }); \ + NSString *selectorName = [PatrolUtils createMethodNameFromPatrolGeneratedGroup:dartTest]; \ + SEL selector = NSSelectorFromString(selectorName); \ + class_addMethod(self, selector, implementation, "v@:"); \ + \ + /* Step 2 – create invocations to the dynamically created methods */ \ + NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector]; \ + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; \ + invocation.selector = selector; \ + \ + NSLog(@"RunnerUITests.testInvocations(): selectorName = %@, signature: %@", selectorName, signature); \ + \ + [invocations addObject:invocation]; \ + } \ + \ + return invocations; \ + } \ + \ + @end\ \ No newline at end of file From 0ce02cd190f8132eddde2048a9f118e11730db7d Mon Sep 17 00:00:00 2001 From: Damian Cudzik Date: Wed, 20 Sep 2023 15:14:53 +0200 Subject: [PATCH 13/21] Fix clang format --- packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h index 8483bf71c..d7f6c0b05 100644 --- a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h +++ b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h @@ -109,4 +109,4 @@ return invocations; \ } \ \ - @end\ \ No newline at end of file + @end\ From d00147e5159e9cfe7f935377fb0d82143fb6a4e8 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Mon, 2 Oct 2023 19:50:10 +0200 Subject: [PATCH 14/21] patrol: bump version to 2.4.0-dev.1 --- packages/patrol/CHANGELOG.md | 4 ++++ packages/patrol/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 2be6a408e..0e8bb1daa 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.0-dev.1 + +- Add support for iOS 11 and 12 (#1733) + ## 2.3.0 - Add support for nested test hierarchies using `group()`s (#1634) diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml index 8d6a92dbd..ae07d235c 100644 --- a/packages/patrol/pubspec.yaml +++ b/packages/patrol/pubspec.yaml @@ -2,7 +2,7 @@ name: patrol description: > Powerful Flutter-native UI testing framework overcoming limitations of existing Flutter testing tools. Ready for action! -version: 2.3.0 +version: 2.4.0-dev.1 homepage: https://patrol.leancode.co repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues From cdb7969dd0d084a4f149cda17e423d24d825fee3 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Tue, 10 Oct 2023 17:48:33 +0200 Subject: [PATCH 15/21] patrol: bump version to 2.4.0-dev.2 --- packages/patrol/CHANGELOG.md | 4 ++++ packages/patrol/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 0e8bb1daa..de98f668e 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.0-dev.2 + +- Fix build-time and run-time crashes when app doesn't use Kotlin 1.8.x (#1782) + ## 2.4.0-dev.1 - Add support for iOS 11 and 12 (#1733) diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml index ae07d235c..b89af934e 100644 --- a/packages/patrol/pubspec.yaml +++ b/packages/patrol/pubspec.yaml @@ -2,7 +2,7 @@ name: patrol description: > Powerful Flutter-native UI testing framework overcoming limitations of existing Flutter testing tools. Ready for action! -version: 2.4.0-dev.1 +version: 2.4.0-dev.2 homepage: https://patrol.leancode.co repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues From 3c81dc244afa04b9410c0eb8fa2379a3340b5107 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Thu, 12 Oct 2023 19:49:05 +0200 Subject: [PATCH 16/21] lower iOS deployment target to 11 --- .../patrol/example/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/patrol/example/ios/Runner.xcodeproj/project.pbxproj b/packages/patrol/example/ios/Runner.xcodeproj/project.pbxproj index db11d1e62..de113a8ae 100644 --- a/packages/patrol/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/patrol/example/ios/Runner.xcodeproj/project.pbxproj @@ -578,7 +578,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -656,7 +656,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -705,7 +705,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; From 45701f681ae13a5d884e0e050b25c50fcd0a0d66 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Thu, 12 Oct 2023 19:50:47 +0200 Subject: [PATCH 17/21] Feature parity: update docs about min iOS version --- docs/native/feature-parity.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/native/feature-parity.mdx b/docs/native/feature-parity.mdx index cd342402d..3d9f1729d 100644 --- a/docs/native/feature-parity.mdx +++ b/docs/native/feature-parity.mdx @@ -35,7 +35,7 @@ impossible to reach 100%. ### Platfom support Patrol works on virtual and physical devices running Android 5.0 (API 21) and -newer, and iOS 13 and newer. +newer, and iOS 11 and newer. [#244]: https://github.com/leancodepl/patrol/issues/244 [#282]: https://github.com/leancodepl/patrol/issues/282 From ce8454d8adb019cbce69306acd17eb46c3e40b91 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Thu, 12 Oct 2023 20:26:10 +0200 Subject: [PATCH 18/21] patrol: bump version to 2.3.1 --- packages/patrol/CHANGELOG.md | 7 ++----- packages/patrol/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index de98f668e..11d12054c 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,10 +1,7 @@ -## 2.4.0-dev.2 - -- Fix build-time and run-time crashes when app doesn't use Kotlin 1.8.x (#1782) - -## 2.4.0-dev.1 +## 2.3.1 - Add support for iOS 11 and 12 (#1733) +- Fix build-time and run-time crashes when app doesn't use Kotlin 1.8.x (#1782) ## 2.3.0 diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml index b89af934e..7704cdc3c 100644 --- a/packages/patrol/pubspec.yaml +++ b/packages/patrol/pubspec.yaml @@ -2,7 +2,7 @@ name: patrol description: > Powerful Flutter-native UI testing framework overcoming limitations of existing Flutter testing tools. Ready for action! -version: 2.4.0-dev.2 +version: 2.3.1 homepage: https://patrol.leancode.co repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues From 06f1d0cc49dfb5294557f430ad2c6514f243571b Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Thu, 12 Oct 2023 23:54:50 +0200 Subject: [PATCH 19/21] Rephrase docs and READMEs (#1798) * fix misleading info about patrol not requiring patrol_cli * rephrase index.mdx to mention LeanCode more * make talk video more visible * Get Patrol now - make this call to action more visible * mention LeanCode in READMEs more * add LeanCode to sidebar * remove links to old articles --- README.md | 3 ++- docs.json | 3 ++- docs/index.mdx | 34 ++++++++++++++++++++++------------ packages/patrol/README.md | 11 +++++++---- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 59b619d2a..5d6ae4b15 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ ![Patrol promotial graphics][promo_graphics] Simple yet powerful Flutter-first UI testing framework overcoming limitations of -`flutter_test`, `integration_test`, and `flutter_driver`. +`flutter_test`, `integration_test`, and `flutter_driver`. Created and supported +by [LeanCode](https://leancode.co). Learn more about Patrol: diff --git a/docs.json b/docs.json index 53054f320..417f1e32a 100644 --- a/docs.json +++ b/docs.json @@ -62,6 +62,7 @@ ] ], ["Effective Patrol", "/effective-patrol"], - ["Tips and tricks", "/tips-and-tricks"] + ["Tips and tricks", "/tips-and-tricks"], + ["LeanCode", "https://leancode.co"] ] } diff --git a/docs/index.mdx b/docs/index.mdx index fcdead616..092e91744 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -3,14 +3,16 @@ title: Main description: The powerful Flutter-first UI testing framework. Ready for action! --- -# About +# Patrol by LeanCode ![Patrol promotial graphics][promo_graphics] -Patrol is a new testing framework for Flutter. It builds on top of Flutter's -existing test tooling to let you do things which were previously impossible. +Patrol is a powerful, open-source testing framework for Flutter apps created and +maintained by [LeanCode] – one of the leading Flutter development consultancies +in the world. Patrol builds on top of Flutter's existing test tooling to enable +you to do things which were previously impossible. -Patrol lets you [access native features of the platform][native] that the +It lets you [access native features of the platform][native] that the Flutter app is running on. Finally, you can interact with permission dialogs, notifications, WebViews, change device settings, toggle Wi-Fi, and much more – and you can code this very easily in plain Dart. @@ -24,17 +26,25 @@ Patrol is also a revamped, production-quality version of Flutter's built-in `integration_test` plugin. It fixes its shortcomings and enables features such as full isolation between tests and sharding. -Patrol is an open-source project which is being developed and maintained by us – -[LeanCode][leancode]! We use it to test our clients' production-grade apps, and -you can do the same! +Patrol is a fully open-source project and we're proud to share it with the +awesome Flutter community. -Learn more about Patrol: +At LeanCode, we use Patrol to test our clients' production-grade apps, and you +can do the same! -- [Read the article about the first public pre-release][article_0x] -- [Read the article about the stable 1.0 release][article_1x] -- [Read the article about the about how Patrol fixes integration_test][article_2x] +[Get Patrol from pub.dev now!][patrol_on_pubdev] -or [get Patrol from pub.dev!][patrol_on_pubdev] +### Learn more about Patrol + +See the talk we gave at Fluttercon 2023: + + + +or [read the article][article_2x] explaining how Patrol fixes deficiencies of +Flutter's official integration_test plugin. + +We also provide professional services related to Flutter app testing – check +them out: [![Automated UI testing services][services_graphics]][services] diff --git a/packages/patrol/README.md b/packages/patrol/README.md index 71d1839dc..6d35d5020 100644 --- a/packages/patrol/README.md +++ b/packages/patrol/README.md @@ -4,12 +4,15 @@ [![codestyle][pub_badge_style]][pub_badge_link] `patrol` package builds on top of `flutter_test` and `integration_test` to make -it easy to control the native UI from Dart test code. +it easy to control the native UI from Dart test code. Created and supported by +[LeanCode](https://leancode.co). -It also provides a new custom finder system to make Flutter widget tests more -concise and understandable, and writing them – faster and more fun. +It must be used together with [patrol_cli]. -It can be used on its own or with [patrol_cli]. +It also provides a new custom finder system to make Flutter widget tests more +concise and understandable, and writing them – faster and more fun. It you want +to only use custom finders, check out +[patrol_finders](https://pub.dev/packages/patrol_finders). ## Installation From 229f76a56408f06c59b7e0d45bd2fea07a0dd5bb Mon Sep 17 00:00:00 2001 From: Guillermo Bueno Vargas Date: Fri, 13 Oct 2023 00:46:25 +0200 Subject: [PATCH 20/21] Patrol CLI: fix generated test bundle being garbled on Windows (#1797) * Removing first slash, closes #1428 * Formatted packages\patrol_cli\lib\src\test_bundler.dart * Update CHANGELOG.md --- packages/patrol_cli/CHANGELOG.md | 4 ++++ packages/patrol_cli/lib/src/test_bundler.dart | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/patrol_cli/CHANGELOG.md b/packages/patrol_cli/CHANGELOG.md index b96c200e3..d79ead66e 100644 --- a/packages/patrol_cli/CHANGELOG.md +++ b/packages/patrol_cli/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- Fix bug with `test_bundle.dart` being sometimes garbled on Windows (#1797) + ## 2.2.0 - Add support for `group()` (#1634) diff --git a/packages/patrol_cli/lib/src/test_bundler.dart b/packages/patrol_cli/lib/src/test_bundler.dart index 4b2c489d9..472eb66b9 100644 --- a/packages/patrol_cli/lib/src/test_bundler.dart +++ b/packages/patrol_cli/lib/src/test_bundler.dart @@ -177,7 +177,10 @@ ${generateGroupsCode([testFilePath]).split('\n').map((e) => ' $e').join('\n')} for (final testFilePath in testFilePaths) { final relativeTestFilePath = _normalizeTestPath(testFilePath); final testName = _createTestName(relativeTestFilePath); - imports.add("import '$relativeTestFilePath' as $testName;"); + final relativeTestFilePathWithoutSlash = relativeTestFilePath[0] == '/' + ? relativeTestFilePath.replaceFirst('/', '') + : relativeTestFilePath; + imports.add("import '$relativeTestFilePathWithoutSlash' as $testName;"); } return imports.join('\n'); From 555609d24f03df3a1f42797ab53a91b643b5639c Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Fri, 13 Oct 2023 01:04:01 +0200 Subject: [PATCH 21/21] patrol_cli: bump version to 2.2.1 (#1803) --- packages/patrol_cli/CHANGELOG.md | 2 +- packages/patrol_cli/lib/src/base/constants.dart | 2 +- packages/patrol_cli/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/patrol_cli/CHANGELOG.md b/packages/patrol_cli/CHANGELOG.md index d79ead66e..fb4d0415e 100644 --- a/packages/patrol_cli/CHANGELOG.md +++ b/packages/patrol_cli/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 2.2.1 - Fix bug with `test_bundle.dart` being sometimes garbled on Windows (#1797) diff --git a/packages/patrol_cli/lib/src/base/constants.dart b/packages/patrol_cli/lib/src/base/constants.dart index 92a6523a0..e16dd10df 100644 --- a/packages/patrol_cli/lib/src/base/constants.dart +++ b/packages/patrol_cli/lib/src/base/constants.dart @@ -1,2 +1,2 @@ /// Version of Patrol CLI. Must be kept in sync with pubspec.yaml. -const version = '2.2.0'; +const version = '2.2.1'; diff --git a/packages/patrol_cli/pubspec.yaml b/packages/patrol_cli/pubspec.yaml index 143b42a7c..3e2e0f8ee 100644 --- a/packages/patrol_cli/pubspec.yaml +++ b/packages/patrol_cli/pubspec.yaml @@ -1,7 +1,7 @@ name: patrol_cli description: > Command-line tool for Patrol, a powerful Flutter-native UI testing framework. -version: 2.2.0 # Must be kept in sync with constants.dart +version: 2.2.1 # Must be kept in sync with constants.dart homepage: https://patrol.leancode.co repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues