Skip to content

Commit

Permalink
[MA-2561] Bad requests for inputText and eraseText if keyboard is not…
Browse files Browse the repository at this point in the history
… present (mobile-dev-inc#2175)
  • Loading branch information
amanjeetsingh150 authored and rasyid7 committed Dec 9, 2024
1 parent ff330bd commit 67d7fc2
Show file tree
Hide file tree
Showing 14 changed files with 61 additions and 19 deletions.
4 changes: 1 addition & 3 deletions maestro-client/src/main/java/maestro/Errors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ sealed class MaestroException(override val message: String) : RuntimeException(m

class UnableToClearState(message: String) : MaestroException(message)

class UnableToPullState(message: String) : MaestroException(message)

class UnableToPushState(message: String) : MaestroException(message)
class UnableToProcessCommand(message: String, val command: String): MaestroException(message)

class AppCrash(message: String): MaestroException(message)

Expand Down
9 changes: 7 additions & 2 deletions maestro-client/src/main/java/maestro/drivers/IOSDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ package maestro.drivers
import com.github.michaelbull.result.expect
import com.github.michaelbull.result.getOrThrow
import com.github.michaelbull.result.onSuccess
import com.github.michaelbull.result.runCatching
import hierarchy.AXElement
import ios.IOSDevice
import ios.IOSDeviceErrors
import maestro.*
import maestro.UiElement.Companion.toUiElement
import maestro.UiElement.Companion.toUiElementOrNull
import maestro.utils.*
import maestro.utils.network.XCUITestServerError
import okio.Sink
import okio.source
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -415,7 +417,6 @@ class IOSDriver(

override fun inputText(text: String) {
metrics.measured("operation", mapOf("command" to "inputText")) {
// silently fail if no XCUIElement has focus
runDeviceCall("inputText") { iosDevice.input(text = text) }
}
}
Expand Down Expand Up @@ -528,7 +529,11 @@ class IOSDriver(
return try {
call()
} catch (socketTimeoutException: SocketTimeoutException) {
throw MaestroException.DriverTimeout("iOS driver timed out while doing $callName call")
LOGGER.error("Got socket timeout processing $callName command", socketTimeoutException)
throw socketTimeoutException
} catch (badRequest: XCUITestServerError.BadRequest) {
LOGGER.error("Bad request for $callName, reason: ${badRequest.errorResponse}")
throw MaestroException.UnableToProcessCommand(command = callName, message = badRequest.error.errorMessage)
} catch (appCrashException: IOSDeviceErrors.AppCrash) {
throw MaestroException.AppCrash(appCrashException.errorMessage)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import maestro.ios.MockXCTestInstaller
import maestro.utils.network.XCUITestServerError
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.SocketPolicy
import maestro.utils.network.Error
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import xcuitest.XCTestClient
import xcuitest.XCTestDriverClient
import xcuitest.api.DeviceInfo
import xcuitest.api.Error
import java.net.InetAddress

class XCTestDriverClientTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package xcuitest
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import hierarchy.ViewHierarchy
import maestro.utils.HttpClient
import maestro.utils.network.Error
import maestro.utils.network.XCUITestServerError
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
Expand Down Expand Up @@ -253,7 +254,7 @@ class XCTestDriverClient(
logger.error("Request for $pathString failed with bad request ${code}, body: $responseBodyAsString")
throw XCUITestServerError.BadRequest(
"Request for $pathString failed with bad request ${code}, body: $responseBodyAsString",
responseBodyAsString
error
)
}
error.errorMessage.contains("Lost connection to the application.*".toRegex()) -> {
Expand Down
Binary file modified maestro-ios-driver/src/main/resources/maestro-driver-ios.zip
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ struct EraseTextHandler: HTTPHandler {
let start = Date()

let appId = RunningApp.getForegroundAppId(requestBody.appIds)
await waitUntilKeyboardIsPresented(appId: appId)
if let errorResponse = await waitUntilKeyboardIsPresented(appId: appId) {
return errorResponse
}

let deleteText = String(repeating: XCUIKeyboardKey.delete.rawValue, count: requestBody.charactersToErase)

Expand All @@ -35,11 +37,16 @@ struct EraseTextHandler: HTTPHandler {
}
}

private func waitUntilKeyboardIsPresented(appId: String?) async {
try? await TimeoutHelper.repeatUntil(timeout: 1, delta: 0.2) {
private func waitUntilKeyboardIsPresented(appId: String?) async -> HTTPResponse? {
let isKeyboardPresented: Bool = (try? await TimeoutHelper.repeatUntil(timeout: 1, delta: 0.2) {
guard let appId = appId else { return true }

return XCUIApplication(bundleIdentifier: appId).keyboards.firstMatch.exists
}) ?? false

// Return an error response if the keyboard is not presented
if !isKeyboardPresented {
return AppError(type: .timeout, message: "Keyboard not presented within 1 second timeout for erase command").httpResponse
}
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ struct InputTextRouteHandler : HTTPHandler {
let start = Date()

let appId = RunningApp.getForegroundAppId(requestBody.appIds)
await waitUntilKeyboardIsPresented(appId: appId)
if let errorResponse = await waitUntilKeyboardIsPresented(appId: appId) {
return errorResponse
}

try await TextInputHelper.inputText(requestBody.text)

Expand All @@ -30,11 +32,16 @@ struct InputTextRouteHandler : HTTPHandler {
}
}

private func waitUntilKeyboardIsPresented(appId: String?) async {
try? await TimeoutHelper.repeatUntil(timeout: 1, delta: 0.2) {
private func waitUntilKeyboardIsPresented(appId: String?) async -> HTTPResponse? {
let isKeyboardPresented: Bool = (try? await TimeoutHelper.repeatUntil(timeout: 1, delta: 0.2) {
guard let appId = appId else { return true }

return XCUIApplication(bundleIdentifier: appId).keyboards.firstMatch.exists
}) ?? false

// Return an error response if the keyboard is not presented
if !isKeyboardPresented {
return AppError(type: .timeout, message: "Keyboard not presented within 1 second timeout for input command").httpResponse
}
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import FlyingFox
enum AppErrorType: String, Codable {
case `internal`
case precondition
case timeout
}

struct AppError: Error, Codable {
Expand All @@ -15,6 +16,7 @@ struct AppError: Error, Codable {
switch type {
case .internal: return .internalServerError
case .precondition: return .badRequest
case .timeout: return .badRequest
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,25 @@ struct TimeoutHelper {
}
}
}

static func repeatUntil(timeout: TimeInterval, delta: TimeInterval, block: () -> Bool) async throws -> Bool {
guard delta >= 0 else {
throw NSError(domain: "Invalid value", code: 1, userInfo: [NSLocalizedDescriptionKey: "Delta cannot be negative"])
}

let timeout = Date().addingTimeInterval(timeout)

while Date() < timeout {
do {
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * delta))
} catch {
throw NSError(domain: "Failed to sleep task", code: 2, userInfo: [NSLocalizedDescriptionKey: "Task could not be put to sleep"])
}

if (block()) {
return true
}
}
return false
}
}
1 change: 1 addition & 0 deletions maestro-utils/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies {
api(libs.square.okio)
implementation(libs.square.okhttp)
implementation(libs.micrometer.core)
api(libs.jackson.module.kotlin)
implementation(libs.micrometer.observation)

testImplementation(libs.mockk)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package xcuitest.api
package maestro.utils.network

import com.fasterxml.jackson.annotation.JsonProperty

Expand Down
2 changes: 1 addition & 1 deletion maestro-utils/src/main/kotlin/network/Errors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ sealed class XCUITestServerError: Throwable() {
data class UnknownFailure(val errorResponse: String) : XCUITestServerError()
data class NetworkError(val errorResponse: String): XCUITestServerError()
data class AppCrash(val errorResponse: String): XCUITestServerError()
data class BadRequest(val errorResponse: String, val clientMessage: String): XCUITestServerError()
data class BadRequest(val errorResponse: String, val error: Error): XCUITestServerError()
}
3 changes: 2 additions & 1 deletion maestro-utils/src/test/kotlin/network/ErrorsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ class ErrorsTest {
fun `XCUITestServerError BadRequest should have correct messages`() {
val errorMessage = "Bad request"
val clientMessage = "Client error"
val error = Error(errorMessage = clientMessage, errorCode = "bad-request-precondition")

assertThrows<XCUITestServerError.BadRequest>(errorMessage) {
throw XCUITestServerError.BadRequest(errorMessage, clientMessage)
throw XCUITestServerError.BadRequest(errorMessage, error)
}
}
}

0 comments on commit 67d7fc2

Please sign in to comment.