Skip to content
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

Reland iOS landscape mode support #1974

Merged
merged 11 commits into from
Sep 9, 2024
Merged
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
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()
Copy link
Collaborator

Choose a reason for hiding this comment

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

this doesnt seem to use the cached info - is that the intent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

If we use the cached version, then swipe will completely break if the device is rotated mid-flow (or when running maestro test in --continuous mode).


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 {
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 advantage of this over the regular private functions from before?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Conciseness, mainly.

If I were to keep using widthPercentToPoint(percent: Double) I'd have to refactor it to widthPercentToPoint(percent: Double, width: Double) which becomes a bit long and repetitive.

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
Loading