Skip to content

Launch app through xctest request instead of simctl #2374 #2421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

Sanskargautam
Copy link

@Sanskargautam Sanskargautam commented Apr 11, 2025

Proposed changes

This pull request transitions the app launch mechanism from simctl to using XCTest APIs. Leveraging XCTest provides a more robust and consistent way to launch apps, with improved compatibility across both simulators and real devices. This change ensures better integration within the XCTest testing lifecycle and enhances support for automation use cases on physical iOS devices.

copilot:summary

Testing

Issues fixed

sanskar.gautam and others added 6 commits April 11, 2025 15:25
* main:
  bump default version to 16 + include ios 18
  feat: make statusBar visible from springboard (mobile-dev-inc#2419)
  use device-model and device-os fields for ios (mobile-dev-inc#2418)
  Reuse xctest (mobile-dev-inc#2413)
  added default analytics (mobile-dev-inc#2417)
  Android WebView devtoools fixes (mobile-dev-inc#2414)
  Updating flying fox on the iOS driver (mobile-dev-inc#2415)
  use os-provided temp dir instead of bash variable (mobile-dev-inc#2412)
  noninteractive ask mode for chatbot (mobile-dev-inc#2409)

# Conflicts:
#	maestro-ios-driver/src/main/resources/maestro-driver-ios.zip
#	maestro-ios-driver/src/main/resources/maestro-driver-iosUITests-Runner.zip
@@ -0,0 +1,5 @@
struct LaunchAppRequest: Codable {
let bundleId: String
let arguments: [String]?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not passed as map here:

data class LaunchAppRequest(val appId: String, val launchArguments: Map<String, Any>, val maestroSessionId: UUID?)

How is this converted to array? Also not sure why we need environment here.

We just need support launch arguments with all type of values mentioned in docs here:

https://docs.maestro.dev/api-reference/commands/launchapp#sending-launch-arguments

Copy link
Collaborator

@amanjeetsingh150 amanjeetsingh150 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d recommend rebasing this branch onto main, or cherry-picking your changes on top of the latest main that way, you’ll be working with the most up-to-date code and can avoid running into major merge conflict.

@@ -90,15 +90,7 @@ class SimctlIOSDevice(
launchArguments: Map<String, Any>,
maestroSessionId: UUID?,
): Result<Unit, Throwable> {
return runCatching {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be removed. We could still be using this somewhere. Some usecases I see is during testing and on cloud.

Copy link
Collaborator

@amanjeetsingh150 amanjeetsingh150 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you plan to test these changes? It would be helpful to include an E2E test that we can run locally.

You can create a new directory under e2e for this. The tests should cover the following scenarios:

  • App launch works on real iOS devices
  • App launch works on simulators
  • App launch arguments work correctly for all of these types: String, Boolean, Double, and Integer

sanskar.gautam added 6 commits May 2, 2025 13:59
* main:
  fix: deserialization breaking for devicectl response (mobile-dev-inc#2458)
  fix: don't use prebuilt for cli on iOS  (mobile-dev-inc#2457)
  [MA 3235] --reinstall-driver flag getting ignored (mobile-dev-inc#2455)
  fix: infinite loop on maestro studio (mobile-dev-inc#2450)
  fix: studio mode should not foreground running app (mobile-dev-inc#2449)
  fix: transport type can be null (mobile-dev-inc#2447)
  [MA 3212] Building iOS driver on host's machines for cli distribution (mobile-dev-inc#2440)
  [MA-3186] Setting up iOS drivers for both real iOS device and simulators (mobile-dev-inc#2429)
  Prepare for release 1.40.0 (mobile-dev-inc#2430)
  adding metadata logs to AI commands (mobile-dev-inc#2428)
  print compact hierarchy as a csv (mobile-dev-inc#2427)
  maestro: interfacing AIEnginePrediction -  making AI commands work on copilot(mobile-dev-inc#2425)

# Conflicts:
#	maestro-ios-driver/src/main/kotlin/device/SimctlIOSDevice.kt
#	maestro-ios-driver/src/main/kotlin/xcuitest/XCTestDriverClient.kt
#	maestro-ios-driver/src/main/kotlin/xcuitest/api/LaunchAppRequest.kt
#	maestro-ios-driver/src/main/resources/maestro-driver-ios.zip
#	maestro-ios-driver/src/main/resources/maestro-driver-iosUITests-Runner.zip
#	maestro-ios-xctest-runner/maestro-driver-ios.xcodeproj/project.pbxproj
#	maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers/LaunchAppHandler.swift
#	maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Models/LaunchAppRequest.swift
#	maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/RouteHandlerFactory.swift
#	maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/XCTestHTTPServer.swift
#	maestro-ios/src/main/java/ios/LocalIOSDevice.kt
#	maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt
* main:
  fix: sharding on iOS (mobile-dev-inc#2467)
  fix: only connected devices for iOS devices (mobile-dev-inc#2463)
  Prepare release 1.40.1 (mobile-dev-inc#2461)
  Fix: Handle empty descriptions in chrome dev tools response (mobile-dev-inc#2452)
  flag to skip interactive device selection by picking a device-index (mobile-dev-inc#2459)
* main:
  use config on e2e
  e2e demoapp config modes
  fix e2e prod test
  fix: hierarchy endpoint causing illegal argument in deep hierarchy screens (mobile-dev-inc#2481)
  [MA 3181] iOS device info frame creating incorrect hierarchy. (mobile-dev-inc#2473)
  Prepare for release 1.40.3 (mobile-dev-inc#2476)
  fix: hardware udid can be missing (mobile-dev-inc#2475)
  [MAE 167] breaking deserialization (mobile-dev-inc#2474)
  Preparing for release 1.40.2 (mobile-dev-inc#2470)

// Verify app is running and arguments are displayed
let app = XCUIApplication(bundleIdentifier: launchRequest.appId)
app.launch()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit unclear on how this test works — do you actually have the app (com.example.example) running locally?

The test is supposed to verify that the app launches, but it looks like we're launching it ourselves in the test, which feels off.

It also seems to test the request payload rather than verifying that the app was launched with the correct arguments.

What I was expecting from this test:

  • The app is actually launched
  • It’s launched with the correct arguments from the request

Right now, it feels more like we’re testing the request structure, not the actual implementation behavior.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the test to assert that the app was successfully launched by sending a request to the local HTTP server and then checking if the app is in the foreground using:

XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5), "App did not launch")
This ensures that the app is actually running after the /launchApp handler is triggered.

Question:

Is there a way in the demo app (or any other app) to verify what launch arguments were passed in, so that we can assert on those as well? Currently, since the app doesn’t expose them, we can only validate the structure of the payload and that the app launched — but not whether the correct arguments were actually used inside the app.

) {
deviceController.launch(id, launchArguments)
launchArguments: Map<String, Any>
): Result<Unit, Throwable> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this return type added here? This doesn't exist on the main

@@ -88,7 +88,7 @@ interface IOSDevice : AutoCloseable {
fun launch(
id: String,
launchArguments: Map<String, Any>,
)
): Result<Unit, Throwable>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be changed. Not sure I understand intent behind this?

case let bool as Bool:
return bool ? ["--\(key)"] : []
default:
return ["--\(key)", "\(value)"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I got this flatmap logic?

Why do we want to check the bool arguments separately here. Can't we just pass arguments as is?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason we're using flatMap with a special case for Bool is because of how launchArguments work in XCUIApplication. It's not a dictionary but an array of strings, similar to command-line arguments.

For boolean flags:

If the value is true, we include it as a standalone flag like --featureFlagA.

If the value is false, we omit it entirely (which is the typical convention for CLI-like flags).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ummm, not sure I still got fully. For example, if we omit it and if someone is expecting false value on their app, will it give them nil or false?

I think right now your test also doesn't seem to cover this case. If you can ensure we have all the types covered in the test that would help here.

}
}
],
"version" : 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test plan got added while i was testing, will remove this commit

@@ -1111,6 +1139,14 @@
version = 0.22.0;
};
};
F4FD416D2DCA33440063BDB3 /* XCRemoteSwiftPackageReference "AnyCodable" */ = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What’s the intent behind this new library? Do we already have an internal API or utility that could be reused instead of adding a new dependency?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent behind using AnyCodable is to support the flexible structure of the launchArguments payload that Maestro sends during the launchApp request. Specifically, Maestro sends this field as a Map<String, Any>, where the values can be of varying types (String, Bool, Int, etc.).

Since Swift’s Codable doesn’t natively support [String: Any], AnyCodable was introduced to cleanly decode this structure without writing extensive boilerplate or custom decoding logic.

I'm happy to switch to an internal utility if we already have something that supports decoding heterogeneous dictionaries like this — let me know if there’s a recommended alternative!

"--retryCount", "3"]

let actual = flattenLaunchArguments(launchRequest.launchArguments)
XCTAssertEqual(actual, expected)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still asserting from request right? How is this asserting what is passed on app com.example.example?

case let bool as Bool:
return bool ? ["--\(key)"] : []
default:
return ["--\(key)", "\(value)"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ummm, not sure I still got fully. For example, if we omit it and if someone is expecting false value on their app, will it give them nil or false?

I think right now your test also doesn't seem to cover this case. If you can ensure we have all the types covered in the test that would help here.

sanskar.gautam added 3 commits May 21, 2025 18:25
…tro into simctl_to_xctest

* 'simctl_to_xctest' of github.com:Sanskargautam/FKiOSMaestro:
  Update driver-verification-tests.xcscheme
  Update driver-verification-tests.xcscheme
* main:
  [MA 3281] Setting location by mocking all the possible providers (mobile-dev-inc#2498)
  feat (maestro): add originalDescription property to commands
  Use MaestroFlowParser to format error messages properly for studio (mobile-dev-inc#2399)
  Do not treat custom config file as a flow file (mobile-dev-inc#2495)
  Do no retry taps by default (mobile-dev-inc#2489)
  revert e2e changes
  [MA 3258] Introducing debug info on element not found exceptions (mobile-dev-inc#2487)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants