Skip to content

[MA 3325] Config to opt for including element behind a modal on iOS #2515

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

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 41 additions & 31 deletions maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import maestro.cli.runner.TestRunner
import maestro.cli.runner.resultview.AnsiResultView
import maestro.cli.session.MaestroSessionManager
import maestro.cli.util.FileUtils.isWebFlow
import maestro.orchestra.workspace.WorkspaceExecutionPlanner
import okio.sink
import picocli.CommandLine
import picocli.CommandLine.Option
Expand Down Expand Up @@ -111,47 +112,56 @@ class RecordCommand : Callable<Int> {
parent?.deviceId
}

val plan = WorkspaceExecutionPlanner.plan(
input = setOf(flowFile.toPath()),
includeTags = emptyList(),
excludeTags = emptyList(),
config = configFile?.toPath()
)

return MaestroSessionManager.newSession(
host = parent?.host,
port = parent?.port,
driverHostPort = null,
deviceId = deviceId,
platform = parent?.platform,
teamId = appleTeamId,
) { session ->
val maestro = session.maestro
val device = session.device

if (flowFile.isDirectory) {
throw CommandLine.ParameterException(
commandSpec.commandLine(),
"Only single Flows are supported by \"maestro record\". $flowFile is a directory.",
)
}

val resultView = AnsiResultView()
val screenRecording = kotlin.io.path.createTempFile(suffix = ".mp4").toFile()
val exitCode = screenRecording.sink().use { out ->
maestro.startScreenRecording(out).use {
TestRunner.runSingle(maestro, device, flowFile, env, resultView, path)
platform = parent?.platform,
executionPlan = plan,
block = { session ->
val maestro = session.maestro
val device = session.device

if (flowFile.isDirectory) {
throw CommandLine.ParameterException(
commandSpec.commandLine(),
"Only single Flows are supported by \"maestro record\". $flowFile is a directory.",
)
}
}

val frames = resultView.getFrames()
val resultView = AnsiResultView()
val screenRecording = kotlin.io.path.createTempFile(suffix = ".mp4").toFile()
val exitCode = screenRecording.sink().use { out ->
maestro.startScreenRecording(out).use {
TestRunner.runSingle(maestro, device, flowFile, env, resultView, path)
}
}

val localOutputFile = outputFile ?: path.resolve("maestro-recording.mp4").toFile()
val videoRenderer = if (local) LocalVideoRenderer(
frameRenderer = SkiaFrameRenderer(),
outputFile = localOutputFile,
outputFPS = 25,
outputWidthPx = 1920,
outputHeightPx = 1080,
) else RemoteVideoRenderer()
videoRenderer.render(screenRecording, frames)
val frames = resultView.getFrames()

TestDebugReporter.deleteOldFiles()
val localOutputFile = outputFile ?: path.resolve("maestro-recording.mp4").toFile()
val videoRenderer = if (local) LocalVideoRenderer(
frameRenderer = SkiaFrameRenderer(),
outputFile = localOutputFile,
outputFPS = 25,
outputWidthPx = 1920,
outputHeightPx = 1080,
) else RemoteVideoRenderer()
videoRenderer.render(screenRecording, frames)

exitCode
}
TestDebugReporter.deleteOldFiles()

exitCode
},
)
}
}
6 changes: 4 additions & 2 deletions maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,9 @@ class TestCommand : Callable<Int> {
): Triple<Int?, Int?, TestExecutionSummary?> {
val driverHostPort = selectPort(effectiveShards)
val deviceId = deviceIds[shardIndex]
val executionPlan = chunkPlans[shardIndex]

logger.info("[shard ${shardIndex + 1}] Selected device $deviceId using port $driverHostPort")
logger.info("[shard ${shardIndex + 1}] Selected device $deviceId using port $driverHostPort with execution plan $executionPlan")

return MaestroSessionManager.newSession(
host = parent?.host,
Expand All @@ -347,6 +348,7 @@ class TestCommand : Callable<Int> {
platform = parent?.platform,
isHeadless = headless,
reinstallDriver = reinstallDriver,
executionPlan = executionPlan
) { session ->
val maestro = session.maestro
val device = session.device
Expand Down Expand Up @@ -457,7 +459,7 @@ class TestCommand : Callable<Int> {
.groupBy { it.index % effectiveShards }
.map { (_, files) ->
val flowsToRun = files.map { it.value }
ExecutionPlan(flowsToRun, plan.sequence)
ExecutionPlan(flowsToRun, plan.sequence, plan.workspaceConfig)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import maestro.utils.CliInsights
import maestro.cli.util.ScreenReporter
import maestro.drivers.AndroidDriver
import maestro.drivers.IOSDriver
import maestro.orchestra.WorkspaceConfig.PlatformConfiguration
import maestro.orchestra.workspace.WorkspaceExecutionPlanner
import org.slf4j.LoggerFactory
import util.IOSDeviceType
import util.XCRunnerCLIUtils
Expand Down Expand Up @@ -71,6 +73,7 @@ object MaestroSessionManager {
isHeadless: Boolean = isStudio,
reinstallDriver: Boolean = true,
deviceIndex: Int? = null,
executionPlan: WorkspaceExecutionPlanner.ExecutionPlan? = null,
block: (MaestroSession) -> T,
): T {
val selectedDevice = selectDevice(
Expand Down Expand Up @@ -112,7 +115,7 @@ object MaestroSessionManager {
isHeadless = isHeadless,
driverHostPort = driverHostPort,
reinstallDriver = reinstallDriver,
prebuiltIOSRunner = false
platformConfiguration = executionPlan?.workspaceConfig?.platform
)
Runtime.getRuntime().addShutdownHook(thread(start = false) {
heartbeatFuture.cancel(true)
Expand Down Expand Up @@ -191,8 +194,8 @@ object MaestroSessionManager {
isStudio: Boolean,
isHeadless: Boolean,
reinstallDriver: Boolean,
prebuiltIOSRunner: Boolean,
driverHostPort: Int?,
platformConfiguration: PlatformConfiguration? = null,
): MaestroSession {
return when {
selectedDevice.device != null -> MaestroSession(
Expand All @@ -209,7 +212,7 @@ object MaestroSessionManager {
driverHostPort,
reinstallDriver,
deviceType = selectedDevice.device.deviceType,
prebuiltRunner = prebuiltIOSRunner
platformConfiguration = platformConfiguration
)

Platform.WEB -> pickWebDevice(isStudio, isHeadless)
Expand All @@ -233,7 +236,8 @@ object MaestroSessionManager {
openDriver = !connectToExistingSession,
driverHostPort = driverHostPort ?: defaultXcTestPort,
reinstallDriver = reinstallDriver,
isStudio = isStudio
isStudio = isStudio,
platformConfiguration = platformConfiguration,
),
device = null,
)
Expand Down Expand Up @@ -298,7 +302,8 @@ object MaestroSessionManager {
openDriver: Boolean,
driverHostPort: Int,
reinstallDriver: Boolean,
isStudio: Boolean
isStudio: Boolean,
platformConfiguration: PlatformConfiguration?,
): Maestro {
val device = PickDeviceInteractor.pickDevice(deviceId, driverHostPort)
return createIOS(
Expand All @@ -307,7 +312,7 @@ object MaestroSessionManager {
driverHostPort,
reinstallDriver,
deviceType = device.deviceType,
prebuiltRunner = isStudio
platformConfiguration = platformConfiguration
)
}

Expand Down Expand Up @@ -337,7 +342,7 @@ object MaestroSessionManager {
openDriver: Boolean,
driverHostPort: Int?,
reinstallDriver: Boolean,
prebuiltRunner: Boolean,
platformConfiguration: PlatformConfiguration?,
deviceType: Device.DeviceType,
): Maestro {

Expand All @@ -356,14 +361,16 @@ object MaestroSessionManager {
IOSDriverConfig(
prebuiltRunner = false,
sourceDirectory = driverPath.pathString,
context = Context.CLI
context = Context.CLI,
snapshotKeyHonorModalViews = platformConfiguration?.ios?.snapshotKeyHonorModalViews
)
}
Device.DeviceType.SIMULATOR -> {
IOSDriverConfig(
prebuiltRunner = prebuiltRunner,
prebuiltRunner = false,
sourceDirectory = "driver-iPhoneSimulator",
context = Context.CLI
context = Context.CLI,
snapshotKeyHonorModalViews = platformConfiguration?.ios?.snapshotKeyHonorModalViews
)
}
else -> throw UnsupportedOperationException("Unsupported device type $deviceType for iOS platform")
Expand Down
1 change: 0 additions & 1 deletion maestro-client/src/main/java/maestro/Driver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package maestro

import okio.Sink
import java.io.File
import java.util.UUID

interface Driver {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ object LocalIOSDeviceController {
)
}

fun launchRunner(deviceId: String, port: Int) {
fun launchRunner(deviceId: String, port: Int, snapshotKeyHonorModalViews: Boolean?) {
val outputFile = File(XCRunnerCLIUtils.logDirectory, "xctest_runner_$date.log")
val params = mutableMapOf("SIMCTL_CHILD_PORT" to port.toString())
if (snapshotKeyHonorModalViews != null) {
params["SIMCTL_CHILD_snapshotKeyHonorModalViews"] = snapshotKeyHonorModalViews.toString()
}
runCommand(
listOf(
"xcrun",
Expand All @@ -41,7 +45,7 @@ object LocalIOSDeviceController {
deviceId,
"dev.mobile.maestro-driver-iosUITests.xctrunner"
),
params = mapOf("SIMCTL_CHILD_PORT" to port.toString()),
params = params,
waitForCompletion = false,
outputFile = outputFile
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,13 @@ object LocalSimulatorUtils {
fun launchUITestRunner(
deviceId: String,
port: Int,
snapshotKeyHonorModalViews: Boolean?,
) {
val outputFile = File(XCRunnerCLIUtils.logDirectory, "xctest_runner_$date.log")
val params = mutableMapOf("SIMCTL_CHILD_PORT" to port.toString())
if (snapshotKeyHonorModalViews != null) {
params["SIMCTL_CHILD_snapshotKeyHonorModalViews"] = snapshotKeyHonorModalViews.toString()
}
runCommand(
listOf(
"xcrun",
Expand All @@ -340,7 +345,7 @@ object LocalSimulatorUtils {
deviceId,
"dev.mobile.maestro-driver-iosUITests.xctrunner"
),
params = mapOf("SIMCTL_CHILD_PORT" to port.toString()),
params = params,
outputFile = outputFile,
waitForCompletion = false,
)
Expand Down
8 changes: 6 additions & 2 deletions maestro-ios-driver/src/main/kotlin/util/XCRunnerCLIUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,14 @@ object XCRunnerCLIUtils {
return runningApps(deviceId)[bundleId]
}

fun runXcTestWithoutBuild(deviceId: String, xcTestRunFilePath: String, port: Int): Process {
fun runXcTestWithoutBuild(deviceId: String, xcTestRunFilePath: String, port: Int, snapshotKeyHonorModalViews: Boolean?): Process {
val date = dateFormatter.format(LocalDateTime.now())
val outputFile = File(logDirectory, "xctest_runner_$date.log")
val logOutputDir = Files.createTempDirectory("maestro_xctestrunner_xcodebuild_output")
val params = mutableMapOf("TEST_RUNNER_PORT" to port.toString())
if (snapshotKeyHonorModalViews != null) {
params["TEST_RUNNER_snapshotKeyHonorModalViews"] = snapshotKeyHonorModalViews.toString()
}
return CommandLineUtils.runCommand(
listOf(
"xcodebuild",
Expand All @@ -137,7 +141,7 @@ object XCRunnerCLIUtils {
),
waitForCompletion = false,
outputFile = outputFile,
params = mapOf("TEST_RUNNER_PORT" to port.toString())
params = params,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package xcuitest.api

data class ViewHierarchyRequest(val appIds: Set<String>, val excludeKeyboardElements: Boolean)
data class ViewHierarchyRequest(
val appIds: Set<String>,
val excludeKeyboardElements: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,12 @@ class LocalXCTestInstaller(
installPrebuiltRunner(deviceId, buildProducts.uiRunnerPath)
} else {
logger.info("Installing driver with xcodebuild")
logger.info("[Start] Running XcUITest with `xcodebuild test-without-building`")
logger.info("[Start] Running XcUITest with `xcodebuild test-without-building` with $defaultPort and config: $iOSDriverConfig")
xcTestProcess = XCRunnerCLIUtils.runXcTestWithoutBuild(
deviceId = this.deviceId,
xcTestRunFilePath = buildProducts.xctestRunPath.absolutePath,
port = defaultPort,
snapshotKeyHonorModalViews = iOSDriverConfig.snapshotKeyHonorModalViews
)
logger.info("[Done] Running XcUITest with `xcodebuild test-without-building`")
}
Expand All @@ -214,11 +215,19 @@ class LocalXCTestInstaller(
when (deviceType) {
IOSDeviceType.REAL -> {
LocalIOSDeviceController.install(deviceId, bundlePath.toPath())
LocalIOSDeviceController.launchRunner(deviceId, defaultPort)
LocalIOSDeviceController.launchRunner(
deviceId = deviceId,
port = defaultPort,
snapshotKeyHonorModalViews = iOSDriverConfig.snapshotKeyHonorModalViews
)
}
IOSDeviceType.SIMULATOR -> {
LocalSimulatorUtils.install(deviceId, bundlePath.toPath())
LocalSimulatorUtils.launchUITestRunner(deviceId, defaultPort)
LocalSimulatorUtils.launchUITestRunner(
deviceId = deviceId,
port = defaultPort,
snapshotKeyHonorModalViews = iOSDriverConfig.snapshotKeyHonorModalViews
)
}
}
}
Expand All @@ -241,7 +250,8 @@ class LocalXCTestInstaller(
data class IOSDriverConfig(
val prebuiltRunner: Boolean,
val sourceDirectory: String,
val context: Context
val context: Context,
val snapshotKeyHonorModalViews: Boolean?
)

companion object {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,25 @@

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-load-method"
#pragma clang diagnostic ignored "-Wcast-function-type-strict"

Check warning on line 64 in maestro-ios-xctest-runner/maestro-driver-iosUITests/Categories/XCAXClient_iOS+FBSnapshotReqParams.m

View workflow job for this annotation

GitHub Actions / Build on Java 11

unknown warning group '-Wcast-function-type-strict', ignored [-Wunknown-warning-option]

Check warning on line 64 in maestro-ios-xctest-runner/maestro-driver-iosUITests/Categories/XCAXClient_iOS+FBSnapshotReqParams.m

View workflow job for this annotation

GitHub Actions / Build on Java 17

unknown warning group '-Wcast-function-type-strict', ignored [-Wunknown-warning-option]

+ (void)load {
// snapshotKeyHonorModalViews to false to make modals and dialogs visible that are invisible otherwise
FBSetCustomParameterForElementSnapshot(@"snapshotKeyHonorModalViews", @0);
NSString *snapshotKeyHonorModalViewsKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"snapshotKeyHonorModalViews"];
if ([snapshotKeyHonorModalViewsKey isEqualToString:@"false"]) {
NSLog(@"Disabling snapshotKeyHonorModalViews to make elements behind modals visible");
FBSetCustomParameterForElementSnapshot(@"snapshotKeyHonorModalViews", @0);

Method original_defaultParametersMethod =
class_getInstanceMethod(self.class, @selector(defaultParameters));
IMP swizzledDefaultParametersImp = (IMP)swizzledDefaultParameters;
original_defaultParameters = (id(*)(id, SEL))method_setImplementation(
original_defaultParametersMethod, swizzledDefaultParametersImp);
Method original_defaultParametersMethod =
class_getInstanceMethod(self.class, @selector(defaultParameters));
IMP swizzledDefaultParametersImp = (IMP)swizzledDefaultParameters;
original_defaultParameters = (id(*)(id, SEL))method_setImplementation(
original_defaultParametersMethod, swizzledDefaultParametersImp);

Method original_snapshotParametersMethod =
class_getInstanceMethod(NSClassFromString(@"XCTElementQuery"),
NSSelectorFromString(@"snapshotParameters"));
IMP swizzledSnapshotParametersImp = (IMP)swizzledSnapshotParameters;
original_snapshotParameters = (id(*)(id, SEL))method_setImplementation(
original_snapshotParametersMethod, swizzledSnapshotParametersImp);
Method original_snapshotParametersMethod = class_getInstanceMethod(NSClassFromString(@"XCTElementQuery"), NSSelectorFromString(@"snapshotParameters"));
IMP swizzledSnapshotParametersImp = (IMP)swizzledSnapshotParameters;
original_snapshotParameters = (id(*)(id, SEL))method_setImplementation(original_snapshotParametersMethod, swizzledSnapshotParametersImp);
}
}

#pragma clang diagnostic pop
Expand Down
Loading
Loading