diff --git a/.github/workflows/prepare-e2e_app.yaml b/.github/workflows/prepare-e2e_app.yaml index 432e1333e..8c9e9ef01 100644 --- a/.github/workflows/prepare-e2e_app.yaml +++ b/.github/workflows/prepare-e2e_app.yaml @@ -53,9 +53,13 @@ jobs: run: .\gradlew.bat :patrol:ktlintFormat - name: Build app with Gradle + env: + MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} run: .\gradlew.bat :app:assembleDebug - name: Build app with Flutter tool + env: + MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} run: flutter build apk --debug prepare-android-on-linux: @@ -108,9 +112,13 @@ jobs: run: ./gradlew :patrol:ktlintFormat - name: Build app with Gradle + env: + MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} run: ./gradlew :app:assembleDebug - name: Build app with Flutter tool + env: + MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} run: flutter build apk --debug prepare-ios: diff --git a/.github/workflows/test-android-emulator-webview.yaml b/.github/workflows/test-android-emulator-webview.yaml index 5bed6fe36..b75742ce8 100644 --- a/.github/workflows/test-android-emulator-webview.yaml +++ b/.github/workflows/test-android-emulator-webview.yaml @@ -79,6 +79,8 @@ jobs: run: dart pub global activate --source path . && patrol - name: Generate Gradle wrapper + env: + MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} run: flutter build apk --config-only - name: Install ew-cli @@ -104,6 +106,8 @@ jobs: echo "INCLUDED_TESTS=$target_paths" >> "$GITHUB_ENV" - name: patrol build android + env: + MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} run: patrol build android --target ${{ env.INCLUDED_TESTS }} --verbose - name: Upload APKs to emulator.wtf and wait for tests to finish diff --git a/.github/workflows/test-android-emulator.yaml b/.github/workflows/test-android-emulator.yaml index 4a210ca6d..e50ec9f27 100644 --- a/.github/workflows/test-android-emulator.yaml +++ b/.github/workflows/test-android-emulator.yaml @@ -86,6 +86,8 @@ jobs: run: dart pub global activate --source path . && patrol - name: Generate Gradle wrapper + env: + MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} run: flutter build apk --config-only - name: Install ew-cli @@ -114,6 +116,8 @@ jobs: echo "EXCLUDED_TESTS=$target_paths" >> "$GITHUB_ENV" - name: patrol build android + env: + MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }} run: | patrol build android --exclude ${{ env.EXCLUDED_TESTS }} \ --dart-define-from-file=defines_1.json --dart-define-from-file=defines_2.json --dart-define-from-file=defines_3.env \ diff --git a/dev/e2e_app/android/app/build.gradle b/dev/e2e_app/android/app/build.gradle index d8a56834f..4e9378f68 100644 --- a/dev/e2e_app/android/app/build.gradle +++ b/dev/e2e_app/android/app/build.gradle @@ -46,6 +46,9 @@ android { targetSdk 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName + manifestPlaceholders = [ + mapsApiKey: System.getenv("MAPS_API_KEY") ?: "" + ] testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner" testInstrumentationRunnerArguments clearPackageData: "true" } diff --git a/dev/e2e_app/android/app/src/main/AndroidManifest.xml b/dev/e2e_app/android/app/src/main/AndroidManifest.xml index 983944a9e..e3762a293 100644 --- a/dev/e2e_app/android/app/src/main/AndroidManifest.xml +++ b/dev/e2e_app/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + @@ -15,7 +16,6 @@ - - - - - - - + + + + + + + + diff --git a/dev/e2e_app/assets/gpx.gpx b/dev/e2e_app/assets/gpx.gpx new file mode 100644 index 000000000..b574bcdd0 --- /dev/null +++ b/dev/e2e_app/assets/gpx.gpx @@ -0,0 +1,327 @@ + + + + 111.32 + + + + 110.89 + + + + 111.72 + + + + 110.47 + + + + 111.35 + + + + 111.03 + + + + 110.53 + + + + 112.25 + + + + 112.34 + + + + 113.82 + + + + 114.30 + + + + 114.14 + + + + 113.86 + + + + 114.07 + + + + 113.28 + + + + 113.33 + + + + 114.31 + + + + 114.36 + + + + 113.55 + + + + 113.87 + + + + 114.18 + + + + 117.12 + + + + 113.66 + + + + 112.99 + + + + 112.65 + + + + 113.75 + + + + 112.92 + + + + 112.75 + + + + 112.60 + + + + 109.12 + + + + 114.16 + + + + 112.46 + + + + 111.91 + + + + 111.26 + + + + 110.33 + + + + 109.99 + + + + 109.22 + + + + 108.61 + + + + 109.28 + + + + 109.78 + + + + 109.47 + + + + 109.46 + + + + 109.48 + + + + 108.79 + + + + 108.41 + + + + 108.22 + + + + 107.92 + + + + 108.02 + + + + 108.61 + + + + 108.32 + + + + 108.78 + + + + 108.33 + + + + 109.24 + + + + 109.20 + + + + 108.98 + + + + 108.94 + + + + 108.80 + + + + 108.74 + + + + 108.19 + + + + 108.37 + + + + 108.07 + + + + 107.68 + + + + 107.57 + + + + 107.20 + + + + 107.53 + + + + 107.37 + + + + 107.40 + + + + 107.76 + + + + 107.73 + + + + 107.96 + + + + 108.02 + + + + 108.16 + + + + 108.15 + + + + 107.96 + + + + 107.23 + + + + 106.60 + + + + 105.78 + + + + 105.74 + + + + 105.09 + + + + 102.46 + + + + 103.25 + + + \ No newline at end of file diff --git a/dev/e2e_app/integration_test/mock_location_test.dart b/dev/e2e_app/integration_test/mock_location_test.dart new file mode 100644 index 000000000..639d631b6 --- /dev/null +++ b/dev/e2e_app/integration_test/mock_location_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/services.dart'; +import 'package:gpx/gpx.dart'; +import 'common.dart'; + +const _timeout = Duration(seconds: 5); // to avoid timeouts on CI + +void main() { + patrol('mock location', ($) async { + await createApp($); + + await $('Open map screen').scrollTo().tap(); + await $.pumpAndSettle(); + + if (await $.native.isPermissionDialogVisible(timeout: _timeout)) { + await $.native.grantPermissionWhenInUse(); + } + + await $.pumpAndSettle(); + + await $.native.setMockLocation(55.2297, 21.0122); + await Future.delayed(const Duration(milliseconds: 100)); + await $.pumpAndSettle(); + expect(await $('Location').waitUntilVisible(), findsOneWidget); + expect(await $('Latitude: 55.2297').waitUntilVisible(), findsOneWidget); + expect(await $('Longitude: 21.0122').waitUntilVisible(), findsOneWidget); + await Future.delayed(const Duration(milliseconds: 1500)); + + await $.native.setMockLocation(55.5297, 21.0122); + await Future.delayed(const Duration(milliseconds: 100)); + await $.pumpAndSettle(); + expect(await $('Location').waitUntilVisible(), findsOneWidget); + expect(await $('Latitude: 55.5297').waitUntilVisible(), findsOneWidget); + expect(await $('Longitude: 21.0122').waitUntilVisible(), findsOneWidget); + await Future.delayed(const Duration(milliseconds: 1500)); + + await $.native.setMockLocation(55.7297, 21.0122); + await Future.delayed(const Duration(milliseconds: 100)); + await $.pumpAndSettle(); + expect(await $('Location').waitUntilVisible(), findsOneWidget); + expect(await $('Latitude: 55.7297').waitUntilVisible(), findsOneWidget); + expect(await $('Longitude: 21.0122').waitUntilVisible(), findsOneWidget); + await Future.delayed(const Duration(milliseconds: 1500)); + }); + + // Skip this test for now, as it's too long to run on CI. + patrol('mock location from GPX', skip: true, ($) async { + await createApp($); + + await $('Open map screen').scrollTo().tap(); + await $.pumpAndSettle(); + + if (await $.native.isPermissionDialogVisible(timeout: _timeout)) { + await $.native.grantPermissionWhenInUse(); + } + + await $.pumpAndSettle(); + + // Load waypoints from GPX file + final waypoints = await loadGpxWaypoints($); + + // Ensure we have at least one waypoint + if (waypoints.isEmpty) { + $.log('No waypoints found in GPX file'); + return; + } else { + $.log('Waypoints found in GPX file: ${waypoints.length}'); + } + + // DateTime? previousTime; + + // Simulate movement through all waypoints + for (final waypoint in waypoints) { + if (waypoint.time == null) { + fail('Waypoint missing timestamp'); + } + + // Unused, but left as an example of how to handle time differences from + // previous point. + // Calculate delay based on time difference from previous point + // if (previousTime != null) { + // final difference = waypoint.time!.difference(previousTime); + // Use actual time difference from GPX file + // await Future.delayed(difference); + // } + // previousTime = waypoint.time; + + await $.native.setMockLocation( + waypoint.lat!, + waypoint.lon!, + ); + + await $.pumpAndSettle(); + await Future.delayed(const Duration(milliseconds: 1500)); + + // Verify location display is updated + expect( + await $('Latitude: ${waypoint.lat!}').waitUntilVisible(), + findsOneWidget, + ); + expect( + await $('Longitude: ${waypoint.lon!}').waitUntilVisible(), + findsOneWidget, + ); + } + }); +} + +Future> loadGpxWaypoints(PatrolIntegrationTester $) async { + // Load GPX file from assets + final gpxString = await rootBundle.loadString('assets/gpx.gpx'); + + // Parse GPX data + final gpx = GpxReader().fromString(gpxString); + $.log('GPX waypoints: ${gpx.wpts.length}'); + return gpx.wpts; +} diff --git a/dev/e2e_app/ios/Flutter/AppFrameworkInfo.plist b/dev/e2e_app/ios/Flutter/AppFrameworkInfo.plist index 7c5696400..163000d85 100644 --- a/dev/e2e_app/ios/Flutter/AppFrameworkInfo.plist +++ b/dev/e2e_app/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 14.0 diff --git a/dev/e2e_app/ios/Podfile b/dev/e2e_app/ios/Podfile index c1edfcaa0..6cb5a582c 100644 --- a/dev/e2e_app/ios/Podfile +++ b/dev/e2e_app/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -ios_deployment_target = '12.0' +ios_deployment_target = '14.0' platform :ios, ios_deployment_target # CocoaPods analytics sends network stats synchronously affecting flutter build latency. diff --git a/dev/e2e_app/ios/Podfile.lock b/dev/e2e_app/ios/Podfile.lock index bb9a73b8f..1750955f6 100644 --- a/dev/e2e_app/ios/Podfile.lock +++ b/dev/e2e_app/ios/Podfile.lock @@ -9,6 +9,17 @@ PODS: - Flutter - geolocator_apple (1.2.0): - Flutter + - Google-Maps-iOS-Utils (5.0.0): + - GoogleMaps (~> 8.0) + - google_maps_flutter_ios (0.0.1): + - Flutter + - Google-Maps-iOS-Utils (< 7.0, >= 5.0) + - GoogleMaps (< 10.0, >= 8.4) + - GoogleMaps (8.4.0): + - GoogleMaps/Maps (= 8.4.0) + - GoogleMaps/Base (8.4.0) + - GoogleMaps/Maps (8.4.0): + - GoogleMaps/Base - patrol (0.0.1): - CocoaAsyncSocket (~> 7.6) - Flutter @@ -25,6 +36,7 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) + - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) - patrol (from `.symlinks/plugins/patrol/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) @@ -32,6 +44,8 @@ DEPENDENCIES: SPEC REPOS: trunk: - CocoaAsyncSocket + - Google-Maps-iOS-Utils + - GoogleMaps EXTERNAL SOURCES: app_links: @@ -44,6 +58,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_timezone/ios" geolocator_apple: :path: ".symlinks/plugins/geolocator_apple/ios" + google_maps_flutter_ios: + :path: ".symlinks/plugins/google_maps_flutter_ios/ios" patrol: :path: ".symlinks/plugins/patrol/darwin" permission_handler_apple: @@ -58,10 +74,13 @@ SPEC CHECKSUMS: flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1 + Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321 + google_maps_flutter_ios: e31555a04d1986ab130f2b9f24b6cdc861acc6d3 + GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d patrol: cf2cd48c7f3e5171610111994f7b466cd76d1f57 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 -PODFILE CHECKSUM: 5424a237bba16bfe751b9ef19da1cf80e4941454 +PODFILE CHECKSUM: 66f59a059824e942cd3cb7be89914fda88260253 COCOAPODS: 1.16.2 diff --git a/dev/e2e_app/ios/Runner.xcodeproj/project.pbxproj b/dev/e2e_app/ios/Runner.xcodeproj/project.pbxproj index e1f1eabee..eb39712a5 100644 --- a/dev/e2e_app/ios/Runner.xcodeproj/project.pbxproj +++ b/dev/e2e_app/ios/Runner.xcodeproj/project.pbxproj @@ -614,7 +614,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -692,7 +692,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -741,7 +741,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/dev/e2e_app/ios/Runner/AppDelegate.swift b/dev/e2e_app/ios/Runner/AppDelegate.swift index 698da0ccd..f49a14021 100644 --- a/dev/e2e_app/ios/Runner/AppDelegate.swift +++ b/dev/e2e_app/ios/Runner/AppDelegate.swift @@ -1,4 +1,5 @@ import Flutter +import GoogleMaps import UIKit @main @@ -7,6 +8,8 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + let mapsApiKey = ProcessInfo.processInfo.environment["MAPS_API_KEY"] ?? "YOUR_API_KEY" + GMSServices.provideAPIKey(mapsApiKey) GeneratedPluginRegistrant.register(with: self) UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/dev/e2e_app/lib/main.dart b/dev/e2e_app/lib/main.dart index a412e23aa..ff5da414c 100644 --- a/dev/e2e_app/lib/main.dart +++ b/dev/e2e_app/lib/main.dart @@ -2,6 +2,7 @@ import 'package:app_links/app_links.dart'; import 'package:e2e_app/applink_screen.dart'; import 'package:e2e_app/loading_screen.dart'; import 'package:e2e_app/location_screen.dart'; +import 'package:e2e_app/map_screen.dart'; import 'package:e2e_app/notifications_screen.dart'; import 'package:e2e_app/overlay_screen.dart'; import 'package:e2e_app/permissions_screen.dart'; @@ -202,6 +203,14 @@ class _ExampleHomePageState extends State { ), child: const Text('Open location screen'), ), + TextButton( + onPressed: () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const MapScreen(), + ), + ), + child: const Text('Open map screen'), + ), TextButton( onPressed: () async => Navigator.of(context).push( MaterialPageRoute( diff --git a/dev/e2e_app/lib/map_screen.dart b/dev/e2e_app/lib/map_screen.dart new file mode 100644 index 000000000..bc69ad94b --- /dev/null +++ b/dev/e2e_app/lib/map_screen.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +class MapScreen extends StatefulWidget { + const MapScreen({super.key}); + + @override + State createState() => _MapScreenState(); +} + +class _MapScreenState extends State { + Position? position; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Map'), + ), + body: Column( + children: [ + const SizedBox(height: 10), + if (position == null) + const Text('Waiting for location...') + else ...[ + Text('Location'), + const SizedBox(height: 10), + Text('Latitude: ${position?.latitude}'), + const SizedBox(height: 10), + Text('Longitude: ${position?.longitude}'), + const SizedBox(height: 10), + ], + Expanded( + child: GoogleMap( + myLocationEnabled: true, + initialCameraPosition: CameraPosition( + target: LatLng(37.7749, -122.4194), + zoom: 12, + ), + onMapCreated: manageLocation, + ), + ), + ], + ), + ); + } + + Future manageLocation(GoogleMapController controller) async { + final permission = await Geolocator.requestPermission(); + + if (permission == LocationPermission.denied) { + return Future.error('Location permissions are denied'); + } + + if (permission == LocationPermission.deniedForever) { + return Future.error( + 'Location permissions are permanently denied, we cannot request permissions.', + ); + } + Geolocator.getPositionStream().listen((position) { + setState(() { + this.position = position; + }); + + controller.animateCamera( + CameraUpdate.newCameraPosition( + CameraPosition( + target: LatLng(position.latitude, position.longitude), + zoom: 12, + ), + ), + ); + }); + } +} diff --git a/dev/e2e_app/pubspec.lock b/dev/e2e_app/pubspec.lock index a4fad28af..55fff9de7 100644 --- a/dev/e2e_app/pubspec.lock +++ b/dev/e2e_app/pubspec.lock @@ -150,6 +150,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -275,6 +283,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.2.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + url: "https://pub.dev" + source: hosted + version: "2.0.24" flutter_test: dependency: "direct dev" description: flutter @@ -357,6 +373,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "4d6e199c561ca06792c964fa24b2bac7197bf4b401c2e1d23e345e5f9939f531" + url: "https://pub.dev" + source: hosted + version: "8.1.1" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: "209856c8e5571626afba7182cf634b2910069dc567954e76ec3e3fb37f5e9db3" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: "1b69fbb3ab76e7a7dfcf25e60f32f81ae5d9b88285343eecb5479116d54be869" + url: "https://pub.dev" + source: hosted + version: "2.14.12" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: "6f798adb0aa1db5adf551f2e39e24bd06c8c0fbe4de912fb2d9b5b3f48147b02" + url: "https://pub.dev" + source: hosted + version: "2.13.2" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: "74bf554a3697765654d3b9140e47e6bff2fdc1e91b8a4f8eafe37de44932423c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: ff39211bd25d7fad125d19f757eba85bd154460907cd4d135e07e3d0f98a4130 + url: "https://pub.dev" + source: hosted + version: "0.5.10" + gpx: + dependency: "direct main" + description: + name: gpx + sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e + url: "https://pub.dev" + source: hosted + version: "2.3.0" gtk: dependency: transitive description: @@ -373,6 +445,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + html: + dependency: transitive + description: + name: html + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + url: "https://pub.dev" + source: hosted + version: "0.15.5" http: dependency: transitive description: @@ -491,7 +571,7 @@ packages: path: "../../packages/patrol" relative: true source: path - version: "3.14.0" + version: "3.15.0" patrol_finders: dependency: transitive description: @@ -588,6 +668,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" rxdart: dependency: transitive description: @@ -596,6 +684,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" shelf: dependency: transitive description: diff --git a/dev/e2e_app/pubspec.yaml b/dev/e2e_app/pubspec.yaml index 47997d7e7..9784ddf15 100644 --- a/dev/e2e_app/pubspec.yaml +++ b/dev/e2e_app/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: flutter_local_notifications: ^17.2.3 flutter_timezone: ^4.1.0 geolocator: ^13.0.0 + google_maps_flutter: ^2.10.0 permission_handler: ^11.4.0 timezone: ^0.9.2 webview_flutter: ^4.4.2 @@ -26,12 +27,15 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter + gpx: ^2.3.0 leancode_lint: ^14.2.0 patrol: path: ../../packages/patrol flutter: uses-material-design: true + assets: + - assets/gpx.gpx patrol: app_name: Patrol example diff --git a/docs/native/feature-parity.mdx b/docs/native/feature-parity.mdx index 629898fe4..203802d7d 100644 --- a/docs/native/feature-parity.mdx +++ b/docs/native/feature-parity.mdx @@ -32,7 +32,7 @@ impossible to reach 100%. macOS support is still in alpha, so it has no native f | Interact with WebView | ⚠️ see [#244] | ✅ | ❌ | | [Press volume down] | ✅ | ✅ (simulator ❌) | ❌ | | [Press volume up] | ✅ | ✅ (simulator ❌) | ❌ | - +| [Set mock location] | ✅ (device ❌) | ✅ | ❌ | [#244]: https://github.com/leancodepl/patrol/issues/244 [#326]: https://github.com/leancodepl/patrol/issues/326 diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 6575610c2..408fa4d3d 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.15.0 + +- Add `setMockLocation` method. (#2547) + ## 3.14.1 - Bump patrol plugin iOS and macOS deployment targets to 12.0. (#2514) diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt index 335a6d217..b1c097d3d 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt @@ -3,8 +3,11 @@ package pl.leancode.patrol import android.app.Instrumentation import android.app.UiAutomation import android.content.Context +import android.content.Context.LOCATION_SERVICE import android.content.Intent +import android.location.Location import android.location.LocationManager +import android.location.provider.ProviderProperties import android.net.Uri import android.os.Build import android.os.SystemClock @@ -299,7 +302,13 @@ class Automator private constructor() { delay() } - fun doubleTap(uiSelector: UiSelector, bySelector: BySelector, index: Int, timeout: Long? = null, delayBetweenTaps: Long? = null) { + fun doubleTap( + uiSelector: UiSelector, + bySelector: BySelector, + index: Int, + timeout: Long? = null, + delayBetweenTaps: Long? = null + ) { Logger.d("doubleTap(): $uiSelector, $bySelector") val uiObject = uiDevice.findObject(uiSelector) @@ -353,7 +362,14 @@ class Automator private constructor() { delay() } - fun enterText(text: String, index: Int, keyboardBehavior: KeyboardBehavior, timeout: Long? = null, dx: Float, dy: Float) { + fun enterText( + text: String, + index: Int, + keyboardBehavior: KeyboardBehavior, + timeout: Long? = null, + dx: Float, + dy: Float + ) { Logger.d("enterText(text: $text, index: $index)") val selector = By.clazz(EditText::class.java) @@ -451,7 +467,12 @@ class Automator private constructor() { delay() } - fun waitUntilVisible(uiSelector: UiSelector, bySelector: BySelector, index: Int, timeout: Long? = null) { + fun waitUntilVisible( + uiSelector: UiSelector, + bySelector: BySelector, + index: Int, + timeout: Long? = null + ) { Logger.d("waitUntilVisible(): $uiSelector, $bySelector") if (waitForView(bySelector, index, timeout) == null) { @@ -679,7 +700,8 @@ class Automator private constructor() { return } - val resourceId = "com.android.permissioncontroller:id/permission_location_accuracy_radio_fine" + val resourceId = + "com.android.permissioncontroller:id/permission_location_accuracy_radio_fine" val uiObject = waitForUiObjectByResourceId(resourceId, timeout = timeoutMillis) ?: throw UiObjectNotFoundException("button to select fine location") @@ -693,7 +715,8 @@ class Automator private constructor() { return } - val resourceId = "com.android.permissioncontroller:id/permission_location_accuracy_radio_coarse" + val resourceId = + "com.android.permissioncontroller:id/permission_location_accuracy_radio_coarse" val uiObject = waitForUiObjectByResourceId(resourceId, timeout = timeoutMillis) ?: throw UiObjectNotFoundException("button to select coarse location") @@ -701,6 +724,35 @@ class Automator private constructor() { uiObject.click() } + fun setMockLocation(latitude: Double, longitude: Double, packageName: String) { + executeShellCommand("appops set $packageName android:mock_location allow") + val locationManager = targetContext.getSystemService(LOCATION_SERVICE) as LocationManager + + val mockLocationProvider = LocationManager.GPS_PROVIDER + locationManager.addTestProvider( + mockLocationProvider, + false, + false, + false, + false, + true, + true, + true, + ProviderProperties.POWER_USAGE_LOW, + ProviderProperties.ACCURACY_FINE + ) + + locationManager.setTestProviderEnabled(mockLocationProvider, true) + val mockLocation = Location(mockLocationProvider) + mockLocation.latitude = latitude + mockLocation.longitude = longitude + mockLocation.altitude = 0.0 + mockLocation.accuracy = 1.0f + mockLocation.time = System.currentTimeMillis() + mockLocation.elapsedRealtimeNanos = System.nanoTime() + locationManager.setTestProviderLocation(mockLocationProvider, mockLocation) + } + /** * Returns true if [bySelector] found a view at [index] within [timeoutMillis], false otherwise. */ diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt index d039689b4..53b92369f 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt @@ -18,6 +18,7 @@ import pl.leancode.patrol.contracts.Contracts.PermissionDialogVisibleRequest import pl.leancode.patrol.contracts.Contracts.PermissionDialogVisibleResponse import pl.leancode.patrol.contracts.Contracts.SetLocationAccuracyRequest import pl.leancode.patrol.contracts.Contracts.SetLocationAccuracyRequestLocationAccuracy +import pl.leancode.patrol.contracts.Contracts.SetMockLocationRequest import pl.leancode.patrol.contracts.Contracts.SwipeRequest import pl.leancode.patrol.contracts.Contracts.TapOnNotificationRequest import pl.leancode.patrol.contracts.Contracts.TapRequest @@ -310,6 +311,10 @@ class AutomatorServer(private val automation: Automator) : NativeAutomatorServer } } + override fun setMockLocation(request: SetMockLocationRequest) { + automation.setMockLocation(request.latitude, request.longitude, request.packageName) + } + override fun debug() { // iOS only } @@ -319,10 +324,18 @@ class AutomatorServer(private val automation: Automator) : NativeAutomatorServer automation.tapOnNotification(request.index.toInt(), timeout = request.timeoutMillis) } else if (request.selector != null) { val selector = request.selector - automation.tapOnNotification(selector.toUiSelector(), selector.toBySelector(), timeout = request.timeoutMillis) + automation.tapOnNotification( + selector.toUiSelector(), + selector.toBySelector(), + timeout = request.timeoutMillis + ) } else if (request.androidSelector != null) { val selector = request.androidSelector - automation.tapOnNotification(selector.toUiSelector(), selector.toBySelector(), timeout = request.timeoutMillis) + automation.tapOnNotification( + selector.toUiSelector(), + selector.toBySelector(), + timeout = request.timeoutMillis + ) } else { throw PatrolException("tapOnNotification(): neither index nor selector are set") } 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 bc3aa8c8f..9dd6d149f 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 @@ -661,4 +661,10 @@ class Contracts { val locationAccuracy: SetLocationAccuracyRequestLocationAccuracy ) + data class SetMockLocationRequest ( + val latitude: Double, + val longitude: Double, + val packageName: String + ) + } diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt index ade144b74..9bbeec755 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 @@ -53,6 +53,7 @@ abstract class NativeAutomatorServer { abstract fun handlePermissionDialog(request: Contracts.HandlePermissionRequest) abstract fun setLocationAccuracy(request: Contracts.SetLocationAccuracyRequest) abstract fun debug() + abstract fun setMockLocation(request: Contracts.SetMockLocationRequest) abstract fun markPatrolAppServiceReady() val router = routes( @@ -235,6 +236,11 @@ abstract class NativeAutomatorServer { debug() Response(OK) }, + "setMockLocation" bind POST to { + val body = json.fromJson(it.bodyString(), Contracts.SetMockLocationRequest::class.java) + setMockLocation(body) + Response(OK) + }, "markPatrolAppServiceReady" bind POST to { markPatrolAppServiceReady() Response(OK) diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift index 6df758e25..e9bb0c0e7 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/Automator.swift @@ -295,6 +295,7 @@ extension Selector { // MARK: Other func debug() throws + func setMockLocation(latitude: Double, longitude: Double) throws } #endif diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift index e833f7944..81711f0ea 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Automator/IOSAutomator.swift @@ -1,5 +1,6 @@ #if PATROL_ENABLED && os(iOS) + import CoreLocation import XCTest import os @@ -867,6 +868,7 @@ for i in 0...150 { let element = self.springboard.descendants(matching: .any).element(boundBy: i) if !element.exists { + break } @@ -885,6 +887,15 @@ } } + func setMockLocation(latitude: Double, longitude: Double) throws { + if #available(iOS 16.4, *) { + runAction("setting mock location to \(latitude), \(longitude)") { + XCUIDevice.shared.location = XCUILocation( + location: CLLocation(latitude: latitude, longitude: longitude)) + } + } + } + // MARK: Private stuff private func clearAndEnterText(data: String, element: XCUIElement, dx: CGFloat, dy: CGFloat) { let currentValue = element.value as? String diff --git a/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift index 4c4bc1e0e..a773253b5 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/AutomatorServer.swift @@ -392,6 +392,12 @@ } } + func setMockLocation(request: SetMockLocationRequest) throws { + return try runCatching { + try automator.setMockLocation(latitude: request.latitude, longitude: request.longitude) + } + } + func debug() throws { return try runCatching { try automator.debug() diff --git a/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift b/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift index c28f73e1a..69b451fc3 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift @@ -382,3 +382,9 @@ public struct SetLocationAccuracyRequest: Codable { public var locationAccuracy: SetLocationAccuracyRequestLocationAccuracy } +public struct SetMockLocationRequest: Codable { + public var latitude: Double + public var longitude: Double + public var packageName: String +} + diff --git a/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift b/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift index d02998b9b..99721a8d2 100644 --- a/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift +++ b/packages/patrol/darwin/Classes/AutomatorServer/NativeAutomatorServer.swift @@ -46,6 +46,7 @@ protocol NativeAutomatorServer { func handlePermissionDialog(request: HandlePermissionRequest) throws func setLocationAccuracy(request: SetLocationAccuracyRequest) throws func debug() throws + func setMockLocation(request: SetMockLocationRequest) throws func markPatrolAppServiceReady() throws } @@ -273,6 +274,12 @@ extension NativeAutomatorServer { return HTTPResponse(.ok) } + private func setMockLocationHandler(request: HTTPRequest) throws -> HTTPResponse { + let requestArg = try JSONDecoder().decode(SetMockLocationRequest.self, from: request.body) + try setMockLocation(request: requestArg) + return HTTPResponse(.ok) + } + private func markPatrolAppServiceReadyHandler(request: HTTPRequest) throws -> HTTPResponse { try markPatrolAppServiceReady() return HTTPResponse(.ok) @@ -481,6 +488,11 @@ extension NativeAutomatorServer { request: request, handler: debugHandler) } + server.route(.POST, "setMockLocation") { + request in handleRequest( + request: request, + handler: setMockLocationHandler) + } server.route(.POST, "markPatrolAppServiceReady") { request in handleRequest( request: request, diff --git a/packages/patrol/example/ios/Runner.xcodeproj/project.pbxproj b/packages/patrol/example/ios/Runner.xcodeproj/project.pbxproj index 83ed28e34..b4851ac11 100644 --- a/packages/patrol/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/patrol/example/ios/Runner.xcodeproj/project.pbxproj @@ -588,6 +588,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -651,6 +652,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -714,6 +716,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -795,6 +798,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = U3EG6EALX7; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -865,6 +869,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -943,6 +948,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = U3EG6EALX7; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1006,6 +1012,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -1082,6 +1089,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = U3EG6EALX7; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1145,6 +1153,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -1226,6 +1235,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = U3EG6EALX7; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1305,6 +1315,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = U3EG6EALX7; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1381,6 +1392,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = U3EG6EALX7; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/packages/patrol/lib/src/native/contracts/contracts.dart b/packages/patrol/lib/src/native/contracts/contracts.dart index b0d55dce0..33fef8460 100644 --- a/packages/patrol/lib/src/native/contracts/contracts.dart +++ b/packages/patrol/lib/src/native/contracts/contracts.dart @@ -1235,3 +1235,28 @@ class SetLocationAccuracyRequest with EquatableMixin { locationAccuracy, ]; } + +@JsonSerializable() +class SetMockLocationRequest with EquatableMixin { + SetMockLocationRequest({ + required this.latitude, + required this.longitude, + required this.packageName, + }); + + factory SetMockLocationRequest.fromJson(Map json) => + _$SetMockLocationRequestFromJson(json); + + final double latitude; + final double longitude; + final String packageName; + + Map toJson() => _$SetMockLocationRequestToJson(this); + + @override + List get props => [ + latitude, + longitude, + packageName, + ]; +} diff --git a/packages/patrol/lib/src/native/contracts/contracts.g.dart b/packages/patrol/lib/src/native/contracts/contracts.g.dart index a69f4d13c..340b9e321 100644 --- a/packages/patrol/lib/src/native/contracts/contracts.g.dart +++ b/packages/patrol/lib/src/native/contracts/contracts.g.dart @@ -791,3 +791,19 @@ const _$SetLocationAccuracyRequestLocationAccuracyEnumMap = { SetLocationAccuracyRequestLocationAccuracy.coarse: 'coarse', SetLocationAccuracyRequestLocationAccuracy.fine: 'fine', }; + +SetMockLocationRequest _$SetMockLocationRequestFromJson( + Map json) => + SetMockLocationRequest( + latitude: (json['latitude'] as num).toDouble(), + longitude: (json['longitude'] as num).toDouble(), + packageName: json['packageName'] as String, + ); + +Map _$SetMockLocationRequestToJson( + SetMockLocationRequest instance) => + { + 'latitude': instance.latitude, + 'longitude': instance.longitude, + 'packageName': instance.packageName, + }; diff --git a/packages/patrol/lib/src/native/contracts/native_automator_client.dart b/packages/patrol/lib/src/native/contracts/native_automator_client.dart index 9f8a580b2..5224dcecd 100644 --- a/packages/patrol/lib/src/native/contracts/native_automator_client.dart +++ b/packages/patrol/lib/src/native/contracts/native_automator_client.dart @@ -340,6 +340,15 @@ class NativeAutomatorClient { ); } + Future setMockLocation( + SetMockLocationRequest request, + ) { + return _sendRequest( + 'setMockLocation', + request.toJson(), + ); + } + Future markPatrolAppServiceReady() { return _sendRequest( 'markPatrolAppServiceReady', diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart index bf949fd51..773591e4b 100644 --- a/packages/patrol/lib/src/native/native_automator.dart +++ b/packages/patrol/lib/src/native/native_automator.dart @@ -974,6 +974,27 @@ class NativeAutomator { ); } + /// Set mock location + /// + /// Works on Android emulator, iOS simulator and iOS real device. Doesn't + /// work on Android real device. + Future setMockLocation( + double latitude, + double longitude, { + String? packageName, + }) async { + await _wrapRequest( + 'setMockLocation latitude: $latitude, longitude: $longitude', + () => _client.setMockLocation( + SetMockLocationRequest( + latitude: latitude, + longitude: longitude, + packageName: packageName ?? _config.packageName, + ), + ), + ); + } + /// Tells the AndroidJUnitRunner that PatrolAppService is ready to answer /// requests about the structure of Dart tests. @internal diff --git a/packages/patrol/lib/src/native/native_automator2.dart b/packages/patrol/lib/src/native/native_automator2.dart index 45d12b197..a629bac67 100644 --- a/packages/patrol/lib/src/native/native_automator2.dart +++ b/packages/patrol/lib/src/native/native_automator2.dart @@ -853,6 +853,27 @@ class NativeAutomator2 { ); } + /// Set mock location + /// + /// Works on Android emulator, iOS simulator and iOS real device. Doesn't + /// work on Android real device. + Future setMockLocation( + double latitude, + double longitude, { + String? packageName, + }) async { + await _wrapRequest( + 'setMockLocation latitude: $latitude, longitude: $longitude', + () => _client.setMockLocation( + SetMockLocationRequest( + latitude: latitude, + longitude: longitude, + packageName: packageName ?? _config.packageName, + ), + ), + ); + } + /// Tells the AndroidJUnitRunner that PatrolAppService is ready to answer /// requests about the structure of Dart tests. @internal diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml index 569a694fc..b013d00c9 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: 3.14.1 +version: 3.15.0 homepage: https://patrol.leancode.co repository: https://github.com/leancodepl/patrol/tree/master/packages/patrol issue_tracker: https://github.com/leancodepl/patrol/issues diff --git a/schema.dart b/schema.dart index c837932d0..37addeb61 100644 --- a/schema.dart +++ b/schema.dart @@ -298,6 +298,12 @@ class SetLocationAccuracyRequest { late SetLocationAccuracyRequestLocationAccuracy locationAccuracy; } +class SetMockLocationRequest { + late double latitude; + late double longitude; + late String packageName; +} + abstract class NativeAutomator { void initialize(); void configure(ConfigureRequest request); @@ -354,6 +360,7 @@ abstract class NativeAutomator { // other void debug(); + void setMockLocation(SetMockLocationRequest request); // TODO(bartekpacia): Move this RPC into a new PatrolNativeTestService service because it doesn't fit here void markPatrolAppServiceReady();