From 0de89fcd17769382928d9512ad9415943fb503b6 Mon Sep 17 00:00:00 2001 From: Siarhei Barysenka Date: Sat, 9 May 2026 23:15:23 +0200 Subject: [PATCH 1/5] [iOS] Include the springboard hierarchy even if the app is in foreground, otherwise the runner can not access push notifications delivered when the app is in foreground --- .../Routes/Handlers/ViewHierarchyHandler.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift index 1561f4dcc7..cb4a062965 100644 --- a/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift +++ b/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/ViewHierarchyHandler.swift @@ -30,10 +30,13 @@ struct ViewHierarchyHandler: HTTPHandler { } NSLog("[Start] View hierarchy snapshot for \(foregroundApp)") let appViewHierarchy = try await getAppViewHierarchy(foregroundApp: foregroundApp, excludeKeyboardElements: requestBody.excludeKeyboardElements) - let viewHierarchy = ViewHierarchy.init(axElement: appViewHierarchy, depth: appViewHierarchy.depth()) - - NSLog("[Done] View hierarchy snapshot for \(foregroundApp) ") - let body = try JSONEncoder().encode(viewHierarchy) + let springboardHierarchy = try elementHierarchy(xcuiElement: springboardApplication) + + let combinedAXElement = AXElement(children: [springboardHierarchy, appViewHierarchy]) + let combinedViewHierarchy = ViewHierarchy(axElement: combinedAXElement, depth: combinedAXElement.depth()) + + NSLog("[Done] View hierarchy snapshot for \(foregroundApp)") + let body = try JSONEncoder().encode(combinedViewHierarchy) return HTTPResponse(statusCode: .ok, body: body) } catch let error as AppError { NSLog("AppError in handleRequest, Error:\(error)"); From 2d4224670cbfad9067dac8ce2fe49d9d51bab223 Mon Sep 17 00:00:00 2001 From: Siarhei Barysenka Date: Sun, 10 May 2026 12:21:03 +0200 Subject: [PATCH 2/5] Add capability to trigger the local notification in the demo_app --- e2e/demo_app/.gitignore | 3 ++ e2e/demo_app/ios/Podfile.lock | 24 ++++++++----- e2e/demo_app/ios/Runner/AppDelegate.swift | 15 ++++++++ e2e/demo_app/lib/main.dart | 44 +++++++++++++++++++++++ e2e/demo_app/pubspec.lock | 32 +++++++++++++++++ e2e/demo_app/pubspec.yaml | 1 + 6 files changed, 110 insertions(+), 9 deletions(-) diff --git a/e2e/demo_app/.gitignore b/e2e/demo_app/.gitignore index d7bc7b56e5..76d01a8792 100644 --- a/e2e/demo_app/.gitignore +++ b/e2e/demo_app/.gitignore @@ -67,3 +67,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/e2e/demo_app/ios/Podfile.lock b/e2e/demo_app/ios/Podfile.lock index 4cda4c5079..9b48d96eb0 100644 --- a/e2e/demo_app/ios/Podfile.lock +++ b/e2e/demo_app/ios/Podfile.lock @@ -6,6 +6,8 @@ PODS: - Flutter (1.0.0) - flutter_launch_arguments (1.0.0): - Flutter + - flutter_local_notifications (0.0.1): + - Flutter - geolocator_apple (1.2.0): - Flutter - FlutterMacOS @@ -27,6 +29,7 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - Flutter (from `Flutter`) - flutter_launch_arguments (from `.symlinks/plugins/flutter_launch_arguments/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -43,6 +46,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_launch_arguments: :path: ".symlinks/plugins/flutter_launch_arguments/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" geolocator_apple: :path: ".symlinks/plugins/geolocator_apple/darwin" integration_test: @@ -57,16 +62,17 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a - connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + app_links: 585674be3c6661708e6cd794ab4f39fb9d8356f9 + connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_launch_arguments: 612fe2bfca02045d0bc01a54cb23cd5a562c9fdf - geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - sensors_plus: 1c5f0a01ce21c609a4df404c4e6879d62bce287f - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb - webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d + flutter_launch_arguments: 79c3d6c461611a8cbaba75da84498b62e88e33a2 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 + geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + sensors_plus: 42b9de1b8237675fa8d8121e4bb93be0f79fa61d + shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 + webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4 PODFILE CHECKSUM: 7aecb2f8279eaf0912ef36bd3b92e7f32ff0fc1a diff --git a/e2e/demo_app/ios/Runner/AppDelegate.swift b/e2e/demo_app/ios/Runner/AppDelegate.swift index de4c9fbef5..7f848f2729 100644 --- a/e2e/demo_app/ios/Runner/AppDelegate.swift +++ b/e2e/demo_app/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import Flutter import UIKit +import UserNotifications @main @objc class AppDelegate: FlutterAppDelegate { @@ -9,6 +10,8 @@ import UIKit ) -> Bool { GeneratedPluginRegistrant.register(with: self) + UNUserNotificationCenter.current().delegate = self + // Set up method channel for password test screen let controller = window?.rootViewController as! FlutterViewController let passwordTestChannel = FlutterMethodChannel( @@ -88,4 +91,16 @@ import UIKit passwordTestVC.modalPresentationStyle = .fullScreen rootViewController.present(passwordTestVC, animated: true) } + + override func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + if #available(iOS 14.0, *) { + completionHandler([.banner, .list, .badge, .sound]) + } else { + completionHandler([.alert, .badge, .sound]) + } + } } \ No newline at end of file diff --git a/e2e/demo_app/lib/main.dart b/e2e/demo_app/lib/main.dart index 1fa55befed..e956d38bf1 100644 --- a/e2e/demo_app/lib/main.dart +++ b/e2e/demo_app/lib/main.dart @@ -24,6 +24,7 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_launch_arguments/flutter_launch_arguments.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'dart:async'; import 'dart:io' show Platform; import 'package:app_links/app_links.dart'; @@ -117,6 +118,45 @@ class _MyHomePageState extends State { }); } + Future _triggerLocalNotification() async { + final plugin = FlutterLocalNotificationsPlugin(); + + await plugin.initialize( + const InitializationSettings( + iOS: DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + ), + android: AndroidInitializationSettings('@mipmap/ic_launcher'), + ), + ); + + await plugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions(alert: true, badge: false, sound: false); + + await plugin.show( + 0, + 'Maestro Local Notification', + 'Hello from Maestro', + const NotificationDetails( + iOS: DarwinNotificationDetails( + presentAlert: true, + presentBanner: true, + presentList: true, + ), + android: AndroidNotificationDetails( + 'demo_app_local_notifications', + 'Local Notifications', + importance: Importance.max, + priority: Priority.high, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -342,6 +382,10 @@ class _MyHomePageState extends State { }, child: const Text('assertScreenshot Threshold'), ), + ElevatedButton( + onPressed: _triggerLocalNotification, + child: const Text('Trigger Local Notification'), + ), ], ), ), diff --git a/e2e/demo_app/pubspec.lock b/e2e/demo_app/pubspec.lock index d92cfb9cd5..da95bf1c66 100644 --- a/e2e/demo_app/pubspec.lock +++ b/e2e/demo_app/pubspec.lock @@ -179,6 +179,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + url: "https://pub.dev" + source: hosted + version: "17.2.4" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" flutter_test: dependency: "direct main" description: flutter @@ -572,6 +596,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" typed_data: dependency: transitive description: diff --git a/e2e/demo_app/pubspec.yaml b/e2e/demo_app/pubspec.yaml index 5e6f402e4e..54ffa16ab4 100644 --- a/e2e/demo_app/pubspec.yaml +++ b/e2e/demo_app/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: flutter_launch_arguments: ^1.0.1 webview_flutter: ^4.13.0 permission_handler: ^11.3.1 + flutter_local_notifications: ^17.2.4 sensors_plus: ^4.0.2 geolocator: ^13.0.1 connectivity_plus: ^7.0.0 From 892850264baeb93f7abeeba8bff395a0f1a4189b Mon Sep 17 00:00:00 2001 From: Siarhei Barysenka Date: Sun, 10 May 2026 12:22:08 +0200 Subject: [PATCH 3/5] Add an E2E test validating the local notification remains accessible for Maestro when displayed over the app running in the foreground --- e2e/demo_app/.maestro/local_notification.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 e2e/demo_app/.maestro/local_notification.yaml diff --git a/e2e/demo_app/.maestro/local_notification.yaml b/e2e/demo_app/.maestro/local_notification.yaml new file mode 100644 index 0000000000..2ddcb545d5 --- /dev/null +++ b/e2e/demo_app/.maestro/local_notification.yaml @@ -0,0 +1,14 @@ +appId: com.example.example +name: Local Notification Visible While App Foregrounded +tags: + - passing + - ios +--- +- launchApp: + clearState: true +- scrollUntilVisible: + element: + text: "Trigger Local Notification" + direction: DOWN +- tapOn: "Trigger Local Notification" +- assertVisible: "Hello from Maestro" From ac62ddc2b42568026f82213eb15fb6ffaa10700c Mon Sep 17 00:00:00 2001 From: Siarhei Barysenka Date: Sun, 10 May 2026 12:54:35 +0200 Subject: [PATCH 4/5] Fix demo_app Android build --- e2e/demo_app/android/app/build.gradle.kts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/e2e/demo_app/android/app/build.gradle.kts b/e2e/demo_app/android/app/build.gradle.kts index 549299748d..e991a9e59c 100644 --- a/e2e/demo_app/android/app/build.gradle.kts +++ b/e2e/demo_app/android/app/build.gradle.kts @@ -11,6 +11,7 @@ android { ndkVersion = flutter.ndkVersion compileOptions { + isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } @@ -39,3 +40,7 @@ android { flutter { source = "../.." } + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") +} From 006ddc3ef7e73a903256d7e984bcca6aeaa08f8f Mon Sep 17 00:00:00 2001 From: Siarhei Barysenka Date: Sun, 10 May 2026 12:59:49 +0200 Subject: [PATCH 5/5] Make the E2E test compatible with Android --- e2e/demo_app/.maestro/local_notification.yaml | 1 - e2e/demo_app/lib/main.dart | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/demo_app/.maestro/local_notification.yaml b/e2e/demo_app/.maestro/local_notification.yaml index 2ddcb545d5..a7abe76508 100644 --- a/e2e/demo_app/.maestro/local_notification.yaml +++ b/e2e/demo_app/.maestro/local_notification.yaml @@ -2,7 +2,6 @@ appId: com.example.example name: Local Notification Visible While App Foregrounded tags: - passing - - ios --- - launchApp: clearState: true diff --git a/e2e/demo_app/lib/main.dart b/e2e/demo_app/lib/main.dart index e956d38bf1..5224aa4e2e 100644 --- a/e2e/demo_app/lib/main.dart +++ b/e2e/demo_app/lib/main.dart @@ -137,6 +137,11 @@ class _MyHomePageState extends State { IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions(alert: true, badge: false, sound: false); + await plugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestNotificationsPermission(); + await plugin.show( 0, 'Maestro Local Notification',