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