Skip to content

Commit

Permalink
Reland iOS landscape mode support (#1974)
Browse files Browse the repository at this point in the history
AndroidDriver has never cached values, instead it was always requesting them
for each new command that used them. Now iOS driver does the same to prevent
old screen size being used for taps/swipes.
  • Loading branch information
bartekpacia authored Sep 9, 2024
1 parent 86d08e6 commit 92c1984
Show file tree
Hide file tree
Showing 16 changed files with 195 additions and 137 deletions.
10 changes: 10 additions & 0 deletions maestro-client/src/main/java/maestro/DeviceInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,13 @@ data class DeviceInfo(
val widthGrid: Int,
val heightGrid: Int,
)

fun xcuitest.api.DeviceInfo.toCommonDeviceInfo(): DeviceInfo {
return DeviceInfo(
platform = Platform.IOS,
widthPixels = widthPixels,
heightPixels = heightPixels,
widthGrid = widthPoints,
heightGrid = heightPoints,
)
}
45 changes: 23 additions & 22 deletions maestro-client/src/main/java/maestro/Maestro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,20 @@ class Maestro(

private val sessionId = UUID.randomUUID()

private val cachedDeviceInfo by lazy {
fetchDeviceInfo()
}

private var screenRecordingInProgress = false
val deviceName: String
get() = driver.name()

fun deviceName(): String {
return driver.name()
val cachedDeviceInfo by lazy {
LOGGER.info("Getting device info")
val deviceInfo = driver.deviceInfo()
LOGGER.info("Got device info: $deviceInfo")
deviceInfo
}

fun deviceInfo(): DeviceInfo {
return cachedDeviceInfo
}
@Deprecated("This function should be removed and its usages refactored. See issue #2031")
fun deviceInfo() = driver.deviceInfo()

private fun fetchDeviceInfo(): DeviceInfo {
LOGGER.info("Getting device info")

return driver.deviceInfo()
}
private var screenRecordingInProgress = false

fun launchApp(
appId: String,
Expand Down Expand Up @@ -129,20 +124,22 @@ class Maestro(
endRelative: String? = null,
duration: Long
) {
val deviceInfo = deviceInfo()

when {
swipeDirection != null -> driver.swipe(swipeDirection, duration)
startPoint != null && endPoint != null -> driver.swipe(startPoint, endPoint, duration)
startRelative != null && endRelative != null -> {
val startPoints = startRelative.replace("%", "")
.split(",").map { it.trim().toInt() }
val startX = cachedDeviceInfo.widthGrid * startPoints[0] / 100
val startY = cachedDeviceInfo.heightGrid * startPoints[1] / 100
val startX = deviceInfo.widthGrid * startPoints[0] / 100
val startY = deviceInfo.heightGrid * startPoints[1] / 100
val start = Point(startX, startY)

val endPoints = endRelative.replace("%", "")
.split(",").map { it.trim().toInt() }
val endX = cachedDeviceInfo.widthGrid * endPoints[0] / 100
val endY = cachedDeviceInfo.heightGrid * endPoints[1] / 100
val endX = deviceInfo.widthGrid * endPoints[0] / 100
val endY = deviceInfo.heightGrid * endPoints[1] / 100
val end = Point(endX, endY)

driver.swipe(start, end, duration)
Expand All @@ -160,8 +157,10 @@ class Maestro(
}

fun swipeFromCenter(swipeDirection: SwipeDirection, durationMs: Long) {
val deviceInfo = deviceInfo()

LOGGER.info("Swiping ${swipeDirection.name} from center")
val center = Point(x = cachedDeviceInfo.widthGrid / 2, y = cachedDeviceInfo.heightGrid / 2)
val center = Point(x = deviceInfo.widthGrid / 2, y = deviceInfo.heightGrid / 2)
driver.swipe(center, swipeDirection, durationMs)
waitForAppToSettle()
}
Expand Down Expand Up @@ -235,8 +234,9 @@ class Maestro(
tapRepeat: TapRepeat? = null,
waitToSettleTimeoutMs: Int? = null
) {
val x = cachedDeviceInfo.widthGrid * percentX / 100
val y = cachedDeviceInfo.heightGrid * percentY / 100
val deviceInfo = driver.deviceInfo()
val x = deviceInfo.widthGrid * percentX / 100
val y = deviceInfo.heightGrid * percentY / 100
tap(
x = x,
y = y,
Expand Down Expand Up @@ -574,6 +574,7 @@ class Maestro(
}

fun waitForAnimationToEnd(timeout: Long?) {
@Suppress("NAME_SHADOWING")
val timeout = timeout ?: ANIMATION_TIMEOUT_MS
LOGGER.info("Waiting for animation to end with timeout $timeout")

Expand Down
1 change: 0 additions & 1 deletion maestro-client/src/main/java/maestro/Point.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,4 @@ data class Point(
fun distance(other: Point): Float {
return distance(x, y, other.x, other.y)
}

}
145 changes: 67 additions & 78 deletions maestro-client/src/main/java/maestro/drivers/IOSDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,6 @@ class IOSDriver(
private val iosDevice: IOSDevice,
) : Driver {

private val deviceInfo by lazy {
iosDevice.deviceInfo()
}

private val widthPoints by lazy {
deviceInfo.widthPoints
}
private val heightPoints by lazy {
deviceInfo.heightPoints
}

private var appId: String? = null
private var proxySet = false

Expand All @@ -73,15 +62,7 @@ class IOSDriver(
}

override fun deviceInfo(): DeviceInfo {
return runDeviceCall {
DeviceInfo(
platform = Platform.IOS,
widthPixels = deviceInfo.widthPixels,
heightPixels = deviceInfo.heightPixels,
widthGrid = deviceInfo.widthPoints,
heightGrid = deviceInfo.heightPoints,
)
}
return runDeviceCall { iosDevice.deviceInfo().toCommonDeviceInfo() }
}

override fun launchApp(
Expand Down Expand Up @@ -200,9 +181,13 @@ class IOSDriver(
}

override fun scrollVertical() {
val deviceInfo = deviceInfo()
val width = deviceInfo.widthGrid
val height = deviceInfo.heightGrid

swipe(
start = Point(widthPercentToPoint(0.5), heightPercentToPoint(0.5)),
end = Point(widthPercentToPoint(0.5), heightPercentToPoint(0.1)),
start = Point(0.5.asPercentOf(width), 0.5.asPercentOf(height)),
end = Point(0.5.asPercentOf(width), 0.1.asPercentOf(height)),
durationMs = 333,
)
}
Expand All @@ -211,32 +196,14 @@ class IOSDriver(
return runDeviceCall { iosDevice.isKeyboardVisible() }
}

private fun validate(start: Point, end: Point): Pair<Point, Point> {
val screenWidth = widthPoints
val screenHeight = heightPoints

val validatedStart = Point(
x = start.x.coerceIn(0, screenWidth),
y = start.y.coerceIn(0, screenHeight)
)

val validatedEnd = Point(
x = end.x.coerceIn(0, screenWidth),
y = end.y.coerceIn(0, screenHeight)
)

return Pair(validatedStart, validatedEnd)
}

override fun swipe(
start: Point,
end: Point,
durationMs: Long
) {
val validatedPoints = validate(start, end)

val startPoint = validatedPoints.first
val endPoint = validatedPoints.second
val deviceInfo = deviceInfo()
val startPoint = start.coerceIn(maxWidth = deviceInfo.widthGrid, maxHeight = deviceInfo.heightGrid)
val endPoint = end.coerceIn(maxWidth = deviceInfo.widthGrid, maxHeight = deviceInfo.heightGrid)

runDeviceCall {
waitForAppToSettle(null, null)
Expand All @@ -251,70 +218,84 @@ class IOSDriver(
}

override fun swipe(swipeDirection: SwipeDirection, durationMs: Long) {
val deviceInfo = deviceInfo()
val width = deviceInfo.widthGrid
val height = deviceInfo.heightGrid

val startPoint: Point
val endPoint: Point

when (swipeDirection) {
SwipeDirection.UP -> {
startPoint = Point(
x = widthPercentToPoint(0.5),
y = heightPercentToPoint(0.9),
x = 0.5.asPercentOf(width),
y = 0.9.asPercentOf(height),
)
endPoint = Point(
x = widthPercentToPoint(0.5),
y = heightPercentToPoint(0.1),
x = 0.5.asPercentOf(width),
y = 0.1.asPercentOf(height),
)
}

SwipeDirection.DOWN -> {
startPoint = Point(
x = widthPercentToPoint(0.5),
y = heightPercentToPoint(0.2),
x = 0.5.asPercentOf(width),
y = 0.2.asPercentOf(height),
)
endPoint = Point(
x = widthPercentToPoint(0.5),
y = heightPercentToPoint(0.9),
x = 0.5.asPercentOf(width),
y = 0.9.asPercentOf(height),
)
}

SwipeDirection.RIGHT -> {
startPoint = Point(
x = widthPercentToPoint(0.1),
y = heightPercentToPoint(0.5),
x = 0.1.asPercentOf(width),
y = 0.5.asPercentOf(height),
)
endPoint = Point(
x = widthPercentToPoint(0.9),
y = heightPercentToPoint(0.5),
x = 0.9.asPercentOf(width),
y = 0.5.asPercentOf(height),
)
}

SwipeDirection.LEFT -> {
startPoint = Point(
x = widthPercentToPoint(0.9),
y = heightPercentToPoint(0.5),
x = 0.9.asPercentOf(width),
y = 0.5.asPercentOf(height),
)
endPoint = Point(
x = widthPercentToPoint(0.1),
y = heightPercentToPoint(0.5),
x = 0.1.asPercentOf(width),
y = 0.5.asPercentOf(height),
)
}
}
swipe(startPoint, endPoint, durationMs)
}

override fun swipe(elementPoint: Point, direction: SwipeDirection, durationMs: Long) {
val deviceInfo = deviceInfo()
val width = deviceInfo.widthGrid
val height = deviceInfo.heightGrid

when (direction) {
SwipeDirection.UP -> {
val end = Point(x = elementPoint.x, y = heightPercentToPoint(0.1))
val end = Point(x = elementPoint.x, y = 0.1.asPercentOf(height))
swipe(elementPoint, end, durationMs)
}

SwipeDirection.DOWN -> {
val end = Point(x = elementPoint.x, y = heightPercentToPoint(0.9))
val end = Point(x = elementPoint.x, y = 0.9.asPercentOf(height))
swipe(elementPoint, end, durationMs)
}

SwipeDirection.RIGHT -> {
val end = Point(x = widthPercentToPoint(0.9), y = elementPoint.y)
val end = Point(x = (0.9).asPercentOf(width), y = elementPoint.y)
swipe(elementPoint, end, durationMs)
}

SwipeDirection.LEFT -> {
val end = Point(x = widthPercentToPoint(0.1), y = elementPoint.y)
val end = Point(x = (0.1).asPercentOf(width), y = elementPoint.y)
swipe(elementPoint, end, durationMs)
}
}
Expand All @@ -323,21 +304,25 @@ class IOSDriver(
override fun backPress() {}

override fun hideKeyboard() {
dismissKeyboardIntroduction()
val deviceInfo = deviceInfo()
val width = deviceInfo.widthGrid
val height = deviceInfo.heightGrid

dismissKeyboardIntroduction(heightPoints = deviceInfo.heightGrid)

if (isKeyboardHidden()) return

swipe(
start = Point(widthPercentToPoint(0.5), heightPercentToPoint(0.5)),
end = Point(widthPercentToPoint(0.5), heightPercentToPoint(0.47)),
start = Point(0.5.asPercentOf(width), 0.5.asPercentOf(height)),
end = Point(0.5.asPercentOf(width), 0.47.asPercentOf(height)),
durationMs = 50,
)

if (isKeyboardHidden()) return

swipe(
start = Point(widthPercentToPoint(0.5), heightPercentToPoint(0.5)),
end = Point(widthPercentToPoint(0.47), heightPercentToPoint(0.5)),
start = Point(0.5.asPercentOf(width), 0.5.asPercentOf(height)),
end = Point(0.47.asPercentOf(width), 0.5.asPercentOf(height)),
durationMs = 50,
)

Expand All @@ -353,8 +338,9 @@ class IOSDriver(
return element == null
}

private fun dismissKeyboardIntroduction() {
val fastTypingInstruction = "Speed up your typing by sliding your finger across the letters to compose a word.*".toRegex()
private fun dismissKeyboardIntroduction(heightPoints: Int) {
val fastTypingInstruction =
"Speed up your typing by sliding your finger across the letters to compose a word.*".toRegex()
val instructionTextFilter = Filters.textMatches(fastTypingInstruction)
val instructionText = MaestroTimer.withTimeout(2000) {
instructionTextFilter(contentDescriptor().aggregate()).firstOrNull()
Expand Down Expand Up @@ -472,14 +458,6 @@ class IOSDriver(
return runDeviceCall { iosDevice.isScreenStatic() }
}

private fun heightPercentToPoint(percent: Double): Int {
return (percent * heightPoints).toInt()
}

private fun widthPercentToPoint(percent: Double): Int {
return (percent * widthPoints).toInt()
}

private fun awaitLaunch() {
val startTime = System.currentTimeMillis()

Expand Down Expand Up @@ -529,3 +507,14 @@ class IOSDriver(
private const val SCREEN_SETTLE_TIMEOUT_MS: Long = 3000
}
}

private fun Double.asPercentOf(total: Int): Int {
return (this * total).toInt()
}

private fun Point.coerceIn(maxWidth: Int, maxHeight: Int): Point {
return Point(
x = x.coerceIn(0, maxWidth),
y = y.coerceIn(0, maxHeight),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class LocalXCTestInstaller(

val checkSuccessful = try {
okHttpClient.newCall(request).execute().use {
logger.info("[Done] Perform XCUITest driver status check on $deviceId")
it.isSuccessful
}
} catch (ignore: IOException) {
Expand Down
Binary file modified maestro-ios-driver/src/main/resources/maestro-driver-ios.zip
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 92c1984

Please sign in to comment.