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

Sync fork #1896

Closed
wants to merge 23 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ bin

# media assets
maestro-orchestra/src/test/resources/media/assets/*

###
.DS_Store
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

=== My Changes ===
## 1.37.4 - 2024-07-29
- [add sleep feature on maestro](https://github.com/rasyid7/maestro/commit/2a8575583af3aaf71de23c7def902db30041fa06)
- [add upload-when-failed option for maestro record](https://github.com/rasyid7/maestro/commit/371f4163cf68de6f5ad5e767990164f2f558315b)
`record --upload-when-failed`
- [add auto-download option for maestro record](https://github.com/rasyid7/maestro/commit/371f4163cf68de6f5ad5e767990164f2f558315b)
`record --auto-download`

## 1.37.3 - 2024-07-23
- fix race condition on android sharding

## 1.37.2 - 2024-07-19
- [fix tags include from OR to AND](https://github.com/rasyid7/maestro/commit/9949f1b204e86dedb3f2912ffdfa65cca2e6121f)

## 1.37.1 - 2024-07-13
- HOTFIX testSingle exit code return 1 for pass and 0 for failed

## 1.37.0 - 2024-07-11
- Feature: Add 'maestro.platform' for javascript to determine platform (mobile-dev-inc#1747)
- Feature: add new test report format: HTML (mobile-dev-inc#1750)
- Feature: [New Feature] Sharding / Parallel Execution (mobile-dev-inc#1732)
- Feature: improve waitForAppToSettle to use waitToSettleTimeoutMs
- Feature: android not wait for settle for faster commands
- Feature: add timestamp for maestro log
- Fix: Remove screen record time limit for Android devices running on API levels >= 34 (mobile-dev-inc#1683)

=======

## 1.37.7

Released on 2024-08-03
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Maestro is the easiest way to automate UI testing for your mobile app.
> [!NOTE]
>
> **Full documentation for Maestro can be found at [maestro.mobile.dev](https://maestro.mobile.dev)**
>
> Since this is forked REPO, to install this maestro, please use
>
> `curl -Ls "https://raw.githubusercontent.com/rasyid7/maestro/main/scripts/install.sh" | bash`

<img src="https://user-images.githubusercontent.com/847683/187275009-ddbdf963-ce1d-4e07-ac08-b10f145e8894.gif" />

Expand Down
141 changes: 94 additions & 47 deletions maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package maestro.cli.command

import kotlinx.coroutines.runBlocking
import maestro.cli.App
import maestro.cli.CliError
import maestro.cli.DisableAnsiMixin
Expand All @@ -29,6 +30,7 @@ import maestro.cli.runner.TestRunner
import maestro.cli.runner.resultview.AnsiResultView
import maestro.cli.session.MaestroSessionManager
import maestro.cli.view.ProgressBar
import maestro.cli.util.FileDownloader
import okio.sink
import org.fusesource.jansi.Ansi
import picocli.CommandLine
Expand Down Expand Up @@ -68,6 +70,18 @@ class RecordCommand : Callable<Int> {
)
private var debugOutput: String? = null

@Option(
names = ["--upload-when-failed"],
description = ["Uploads the video to Maestro only if the test fails"]
)
private var uploadIfFailed: Boolean = false

@Option(
names = ["--auto-download"],
description = ["Automatically download the video after the render is completed"]
)
private var autoDownload: Boolean = false

override fun call(): Int {
if (!flowFile.exists()) {
throw CommandLine.ParameterException(
Expand Down Expand Up @@ -108,68 +122,101 @@ class RecordCommand : Callable<Int> {
}
}

System.err.println()
System.err.println("@|bold Rendering your video. This usually takes a couple minutes...|@".render())
System.err.println()
val upload = !uploadIfFailed || (this.uploadIfFailed && (exitCode != 0))

val frames = resultView.getFrames()
val client = ApiClient("")
if (upload) {
System.err.println()
System.err.println("@|bold Rendering your video. This usually takes a couple minutes...|@".render())
System.err.println()

val uploadProgress = ProgressBar(50)
System.err.println("Uploading raw files for render...")
val id = client.render(screenRecording, frames) { totalBytes, bytesWritten ->
uploadProgress.set(bytesWritten.toFloat() / totalBytes)
}
System.err.println()
val frames = resultView.getFrames()
val client = ApiClient("")

val uploadProgress = ProgressBar(50)
System.err.println("Uploading raw files for render...")
val id = client.render(screenRecording, frames) { totalBytes, bytesWritten ->
uploadProgress.set(bytesWritten.toFloat() / totalBytes)
}
System.err.println()

var renderProgress: ProgressBar? = null
var status: String? = null
var positionInQueue: Int? = null
var downloadUrl: String? = null

var renderProgress: ProgressBar? = null
var status: String? = null
var positionInQueue: Int? = null
while (true) {
val state = client.getRenderState(id)
while (true) {
val state = client.getRenderState(id)

// If new position or status, print header
if (state.status != status || state.positionInQueue != positionInQueue) {
status = state.status
positionInQueue = state.positionInQueue
// If new position or status, print header
if (state.status != status || state.positionInQueue != positionInQueue) {
status = state.status
positionInQueue = state.positionInQueue

if (renderProgress != null) {
renderProgress.set(1f)
System.err.println()
}

if (renderProgress != null) {
renderProgress.set(1f)
System.err.println()

System.err.println("Status : ${styledStatus(state.status)}")
if (state.positionInQueue != null) {
System.err.println("Position In Queue : ${state.positionInQueue}")
}
}

System.err.println()
// Add ticks to progress bar
if (state.currentTaskProgress != null) {
if (renderProgress == null) renderProgress = ProgressBar(50)
renderProgress.set(state.currentTaskProgress)
}

System.err.println("Status : ${styledStatus(state.status)}")
if (state.positionInQueue != null) {
System.err.println("Position In Queue : ${state.positionInQueue}")
// Print download url or error and return
if (state.downloadUrl != null || state.error != null) {
System.err.println()
if (state.downloadUrl != null) {
System.err.println("@|bold Signed Download URL:|@".render())
System.err.println()
print("@|cyan,bold ${state.downloadUrl}|@".render())
System.err.println()
System.err.println()
System.err.println("Open the link above to download your video. If you're sharing on Twitter be sure to tag us @|bold @mobile__dev|@!".render())
downloadUrl = state.downloadUrl
} else {
System.err.println("@|bold Render encountered during rendering:|@".render())
System.err.println(state.error)
}
break
}
}

// Add ticks to progress bar
if (state.currentTaskProgress != null) {
if (renderProgress == null) renderProgress = ProgressBar(50)
renderProgress.set(state.currentTaskProgress)
Thread.sleep(2000)
}

// Print download url or error and return
if (state.downloadUrl != null || state.error != null) {
System.err.println()
if (state.downloadUrl != null) {
System.err.println("@|bold Signed Download URL:|@".render())
System.err.println()
print("@|cyan,bold ${state.downloadUrl}|@".render())
System.err.println()
System.err.println()
System.err.println("Open the link above to download your video. If you're sharing on Twitter be sure to tag us @|bold @mobile__dev|@!".render())
} else {
System.err.println("@|bold Render encountered during rendering:|@".render())
System.err.println(state.error)
if (autoDownload && downloadUrl != null) {
println("Downloading Video...")
val downloadFile = File(flowFile.name.removeSuffix(".yaml") + ".mp4")
runBlocking {
FileDownloader.downloadFile(downloadUrl, downloadFile).collect { result ->
when (result) {
is FileDownloader.DownloadResult.Success -> {
System.err.println("Download completed: ${downloadFile.absolutePath}")
}
is FileDownloader.DownloadResult.Error -> {
System.err.println("Download failed: ${result.message}")
}
is FileDownloader.DownloadResult.Progress -> {
// Do nothing
}
}
}
}
break
} else {
println("Not downloading video since autoDownload is $autoDownload and downloadUrl is ${downloadUrl}")
}

Thread.sleep(2000)
} else {
System.err.println()
System.err.println("@|bold Not uploading video since the test is PASS |@".render())
System.err.println()
}

TestDebugReporter.deleteOldFiles()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ object MaestroSessionManager {
val heartbeatFuture = executor.scheduleAtFixedRate(
{
try {
Thread.sleep(1000) // Add a 1-second delay here for fixing race condition
SessionStore.heartbeat(sessionId, selectedDevice.platform)
} catch (e: Exception) {
logger.error("Failed to record heartbeat", e)
Expand Down
14 changes: 13 additions & 1 deletion maestro-client/src/main/java/maestro/Maestro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class Maestro(private val driver: Driver) : AutoCloseable {
) {
LOGGER.info("Tapping on element: ${tapRepeat ?: ""} $element")

val hierarchyBeforeTap = waitForAppToSettle(initialHierarchy, appId, waitToSettleTimeoutMs) ?: initialHierarchy
val hierarchyBeforeTap = initialHierarchy

val center = (
hierarchyBeforeTap
Expand Down Expand Up @@ -350,6 +350,12 @@ class Maestro(private val driver: Driver) : AutoCloseable {
} else {
driver.tap(Point(x, y))
}

if (waitToSettleTimeoutMs != null && waitToSettleTimeoutMs < 100) {
LOGGER.info("waitToSettleTimeoutMs is less than 100, skip get hierarchy")
return
}

val hierarchyAfterTap = waitForAppToSettle(waitToSettleTimeoutMs = waitToSettleTimeoutMs)

if (hierarchyBeforeTap != hierarchyAfterTap) {
Expand Down Expand Up @@ -564,6 +570,12 @@ class Maestro(private val driver: Driver) : AutoCloseable {
ScreenshotUtils.waitUntilScreenIsStatic(timeout, SCREENSHOT_DIFF_THRESHOLD, driver)
}

fun sleep(time: Long?) {
val time = time ?: ANIMATION_TIMEOUT_MS
LOGGER.info("Sleep for $time ms")
Thread.sleep(time)
}

fun setProxy(
host: String = SocketUtils.localIp(),
port: Int
Expand Down
2 changes: 1 addition & 1 deletion maestro-client/src/main/java/maestro/debuglog/LogConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.slf4j.LoggerFactory
import java.util.Properties

object LogConfig {
private const val LOG_PATTERN = "[%-5level] %logger{36} - %msg%n"
private const val LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n"

fun configure(logFileName: String) {
val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
Expand Down
5 changes: 3 additions & 2 deletions maestro-client/src/main/java/maestro/drivers/IOSDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,9 @@ class IOSDriver(
}

override fun waitForAppToSettle(initialHierarchy: ViewHierarchy?, appId: String?, timeoutMs: Int?): ViewHierarchy? {
LOGGER.info("Waiting for animation to end with timeout $SCREEN_SETTLE_TIMEOUT_MS")
val didFinishOnTime = waitUntilScreenIsStatic(SCREEN_SETTLE_TIMEOUT_MS)
val timeOut = timeoutMs?.toLong() ?: SCREEN_SETTLE_TIMEOUT_MS
LOGGER.info("Waiting for animation to end with timeout $timeOut")
val didFinishOnTime = waitUntilScreenIsStatic(timeOut)

return if (didFinishOnTime) null else ScreenshotUtils.waitForAppToSettle(initialHierarchy, this, timeoutMs)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,20 @@ data class WaitForAnimationToEndCommand(
}
}

data class SleepCommand(
val time: Long?,
val label: String? = null,
) : Command {

override fun description(): String {
return label ?: "Sleep for $time ms"
}

override fun evaluateScripts(jsEngine: JsEngine): Command {
return this
}
}

data class EvalScriptCommand(
val scriptString: String,
val label: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ data class MaestroCommand(
val addMediaCommand: AddMediaCommand? = null,
val setAirplaneModeCommand: SetAirplaneModeCommand? = null,
val toggleAirplaneModeCommand: ToggleAirplaneModeCommand? = null,
val sleepCommand: SleepCommand? = null,
) {

constructor(command: Command) : this(
Expand Down Expand Up @@ -105,6 +106,7 @@ data class MaestroCommand(
addMediaCommand = command as? AddMediaCommand,
setAirplaneModeCommand = command as? SetAirplaneModeCommand,
toggleAirplaneModeCommand = command as? ToggleAirplaneModeCommand,
sleepCommand = command as? SleepCommand,
)

fun asCommand(): Command? = when {
Expand Down Expand Up @@ -145,6 +147,7 @@ data class MaestroCommand(
addMediaCommand != null -> addMediaCommand
setAirplaneModeCommand != null -> setAirplaneModeCommand
toggleAirplaneModeCommand != null -> toggleAirplaneModeCommand
sleepCommand != null -> sleepCommand
else -> null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ class Orchestra(
is AddMediaCommand -> addMediaCommand(command.mediaPaths)
is SetAirplaneModeCommand -> setAirplaneMode(command)
is ToggleAirplaneModeCommand -> toggleAirplaneMode()
is SleepCommand -> sleepCommand(command)
else -> true
}.also { mutating ->
if (mutating) {
Expand Down Expand Up @@ -368,6 +369,11 @@ class Orchestra(
return true
}

private fun sleepCommand(command: SleepCommand): Boolean {
maestro.sleep(command.time)
return true
}

private fun defineVariablesCommand(command: DefineVariablesCommand): Boolean {
command.env.forEach { (name, value) ->
jsEngine.putEnv(name, value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,17 @@ object WorkspaceExecutionPlanner {
val config = configPerFlowFile[it]
val tags = config?.tags ?: emptyList()

(allIncludeTags.isEmpty() || tags.any(allIncludeTags::contains))
(allIncludeTags.isEmpty() || allIncludeTags.all { includeTag -> tags.contains(includeTag) })
&& (allExcludeTags.isEmpty() || !tags.any(allExcludeTags::contains))
}

println("")
println("Number flows to run: ${allFlows.size}")
println("Flows to run:")
for (flow in allFlows) {
println(" * ${flow.fileName.toString().substringBeforeLast(".")}")
}

if (allFlows.isEmpty()) {
throw ValidationError("Include / Exclude tags did not match any Flows:\n\nInclude Tags:\n${toYamlListString(allIncludeTags)}\n\nExclude Tags:\n${toYamlListString(allExcludeTags)}")
}
Expand Down
Loading
Loading