Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into FEAT-custom-labels
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Caseley authored and igorsmotto committed Sep 12, 2023
2 parents 0a7085e + 319e466 commit faa32a6
Show file tree
Hide file tree
Showing 67 changed files with 1,014 additions and 152 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ bin

.idea

# media assets
maestro-orchestra/src/test/resources/media/assets/*
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Changelog

## 1.32.0 - 20223-09-06

Studio
- Feature: Support writing Flows using AI (more info to come 🚀)
- Feature: Maestro Studio can now run in multiple tabs simultaneously
- Feature: Added element id and copy option for it
- Tweak: Hide action buttons till command is hovered
- Tweak: Hide Unnecessary Scrollbars
- Tweak: Repl view scroll improvements
- Tweak: Improve Maestro Studio performance
- Fix: Selected element size
- Fix: Performance issues with maestro studio device refresh
- Fix: Fixed dark mode for element id

CLI
- Feature: New command to start or create a Maestro recommended device (docs)
- Feature: Support id selection for testID with react-native-web (community contribution)
- Feature: Control if browser automatically opens when running Maestro Studio via --no-window (community contribution)
- Tweak: Show cancellation reason when available (Maestro Cloud)
- Tweak: Update selenium-java and remove webdrivermanager to support Chrome 116+
- Tweak: Show device type when running on Maestro Cloud
- Tweak: Added better messaging and recovery options for Maestro Cloud uploads (useful for CI)
- Tweak: Added better error messages for missing workspace and yaml validation errors
- Tweak: Added file name and line number in yaml parsing error messages
- Fix: Input text and erase text stability improvements for iOS
- Fix: Leaking response body on iOS & better error handling for iOS Driver
- Fix: Fixed Maestro Cloud wrong exit code when flow failed
- Fix: Debug commands parsing would crash maestro
- Fix: Cleaning up debug logs




## 1.31.0 - 2023-08-10

- Fix: Warning shown from OkHttp for leaking response bodies on CLI
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official
GROUP=dev.mobile
VERSION_NAME=1.31.0
VERSION_NAME=1.32.0
POM_DESCRIPTION=Maestro is a server-driven platform-agnostic library that allows to drive tests for both iOS and Android using the same implementation through an intuitive API.
POM_URL=https://github.com/mobile-dev-inc/maestro
POM_SCM_URL=https://github.com/mobile-dev-inc/maestro
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package dev.mobile.maestro
import android.app.UiAutomation
import android.content.Context
import android.content.Context.LOCATION_SERVICE
import android.content.Intent
import android.graphics.Bitmap
import android.location.Criteria
import android.location.Location
Expand Down Expand Up @@ -46,22 +45,14 @@ import androidx.test.uiautomator.Configurator
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiDeviceExt.clickExt
import com.google.protobuf.ByteString
import io.grpc.Status
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder
import io.grpc.stub.StreamObserver
import maestro_android.MaestroAndroid
import maestro_android.MaestroDriverGrpc
import maestro_android.checkWindowUpdatingResponse
import maestro_android.deviceInfo
import maestro_android.eraseAllTextResponse
import maestro_android.inputTextResponse
import maestro_android.launchAppResponse
import maestro_android.screenshotResponse
import maestro_android.setLocationResponse
import maestro_android.tapResponse
import maestro_android.viewHierarchyResponse
import maestro_android.*
import org.junit.Test
import org.junit.runner.RunWith
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import kotlin.system.measureTimeMillis

/**
Expand Down Expand Up @@ -217,6 +208,33 @@ class Service(
responseObserver.onCompleted()
}

override fun addMedia(responseObserver: StreamObserver<MaestroAndroid.AddMediaResponse>): StreamObserver<MaestroAndroid.AddMediaRequest> {
return object : StreamObserver<MaestroAndroid.AddMediaRequest> {

var outputStream: OutputStream? = null

override fun onNext(value: MaestroAndroid.AddMediaRequest) {
if (outputStream == null) {
outputStream = MediaStorage.getOutputStream(
value.mediaName,
value.mediaExt
)
}
value.payload.data.writeTo(outputStream)
}

override fun onError(t: Throwable) {
responseObserver.onError(t.internalError())
}

override fun onCompleted() {
responseObserver.onNext(addMediaResponse { })
responseObserver.onCompleted()
}

}
}

override fun eraseAllText(
request: MaestroAndroid.EraseAllTextRequest,
responseObserver: StreamObserver<MaestroAndroid.EraseAllTextResponse>
Expand Down Expand Up @@ -396,7 +414,13 @@ class Service(
uiDevice.pressKeyCode(keyCode, META_SHIFT_LEFT_ON)
}

companion object {
private const val LENGTH_KEY_VALUE_PAIR = 2
internal fun Throwable.internalError() = Status.INTERNAL.withDescription(message).asException()

enum class FileType(val ext: String, val mimeType: String) {
JPG("jpg", "image/jpg"),
JPEG("jpeg", "image/jpeg"),
PNG("png", "image/png"),
GIF("gif", "image/gif"),
MP4("mp4", "video/mp4"),
}
}
31 changes: 31 additions & 0 deletions maestro-android/src/androidTest/java/dev/mobile/maestro/Media.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dev.mobile.maestro

import android.content.ContentValues
import android.provider.MediaStore
import androidx.test.platform.app.InstrumentationRegistry
import java.io.OutputStream

object MediaStorage {

fun getOutputStream(mediaName: String, mediaExt: String): OutputStream? {
val uri = when (mediaExt) {
Service.FileType.JPG.ext,
Service.FileType.PNG.ext,
Service.FileType.GIF.ext,
Service.FileType.JPEG.ext -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
Service.FileType.MP4.ext -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
else -> throw IllegalStateException("mime .$mediaExt not yet supported")
}
val ext = Service.FileType.values().first { it.ext == mediaExt }
val contentValues = ContentValues()
contentValues.apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, mediaName)
put(MediaStore.MediaColumns.MIME_TYPE, ext.mimeType)
}
val contentResolver = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver
val outputStream = contentResolver.insert(uri, contentValues)?.let {
contentResolver.openOutputStream(it)
}
return outputStream
}
}
2 changes: 1 addition & 1 deletion maestro-cli/gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
CLI_VERSION=1.31.0
CLI_VERSION=1.32.0
4 changes: 4 additions & 0 deletions maestro-client/src/main/java/maestro/Driver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,8 @@ interface Driver {
fun capabilities(): List<Capability>

fun setPermissions(appId: String, permissions: Map<String, String>)

fun addMedia(mediaFiles: List<File>)

fun removeMedia()
}
19 changes: 19 additions & 0 deletions maestro-client/src/main/java/maestro/Maestro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ import maestro.utils.SocketUtils
import okio.Sink
import okio.buffer
import okio.sink
import okio.source
import org.slf4j.LoggerFactory
import java.awt.image.BufferedImage
import java.io.File
import java.nio.file.Path
import java.util.UUID
import kotlin.system.measureTimeMillis

Expand All @@ -45,6 +47,7 @@ class Maestro(private val driver: Driver) : AutoCloseable {
}

private var screenRecordingInProgress = false
private var isMediaAdded = false

fun deviceName(): String {
return driver.name()
Expand Down Expand Up @@ -458,6 +461,22 @@ class Maestro(private val driver: Driver) : AutoCloseable {
waitForAppToSettle()
}

fun addMedia(fileNames: List<String>) {
val mediaFiles = fileNames.map { File(it) }
driver.addMedia(mediaFiles)
isMediaAdded = true
}

fun removeMedia() {
if (isMediaAdded) {
driver.removeMedia()
}
}

fun shouldResetMedia(): Boolean {
return isMediaAdded
}

override fun close() {
driver.close()
}
Expand Down
13 changes: 13 additions & 0 deletions maestro-client/src/main/java/maestro/Media.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package maestro

import okio.Source

class NamedSource(val name: String, val source: Source, val extension: String, val path: String)

enum class MediaExt(val extName: String) {
PNG("png"),
JPEG("jpeg"),
JPG("jpg"),
GIF("gif"),
MP4("mp4"),
}
10 changes: 10 additions & 0 deletions maestro-client/src/main/java/maestro/device/DeviceConfigManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package maestro.device

import maestro.Maestro

class DeviceConfigManager(private val maestro: Maestro) {

fun resetMedia() {
maestro.removeMedia()
}
}
84 changes: 57 additions & 27 deletions maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,23 @@

package maestro.drivers

import com.google.protobuf.ByteString
import dadb.AdbShellPacket
import dadb.AdbShellResponse
import dadb.AdbShellStream
import dadb.Dadb
import io.grpc.ManagedChannelBuilder
import maestro.Capability
import maestro.DeviceInfo
import maestro.Driver
import maestro.Filters
import maestro.KeyCode
import maestro.Maestro
import maestro.Platform
import maestro.Point
import maestro.ScreenRecording
import maestro.SwipeDirection
import maestro.TreeNode
import maestro.UiElement
import maestro.*
import maestro.UiElement.Companion.toUiElementOrNull
import maestro.ViewHierarchy
import maestro.android.AndroidAppFiles
import maestro.android.AndroidLaunchArguments.toAndroidLaunchArguments
import maestro.utils.BlockingStreamObserver
import maestro.utils.MaestroTimer
import maestro.utils.ScreenshotUtils
import maestro.utils.StringUtils.toRegexSafe
import maestro_android.MaestroDriverGrpc
import maestro_android.checkWindowUpdatingRequest
import maestro_android.deviceInfoRequest
import maestro_android.eraseAllTextRequest
import maestro_android.inputTextRequest
import maestro_android.launchAppRequest
import maestro_android.screenshotRequest
import maestro_android.setLocationRequest
import maestro_android.tapRequest
import maestro_android.viewHierarchyRequest
import maestro_android.*
import net.dongliu.apk.parser.ApkFile
import okio.Sink
import okio.buffer
import okio.sink
import okio.source
import okio.*
import org.slf4j.LoggerFactory
import org.w3c.dom.Element
import org.w3c.dom.Node
Expand All @@ -69,6 +47,7 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.io.use

class AndroidDriver(
private val dadb: Dadb,
Expand All @@ -79,6 +58,7 @@ class AndroidDriver(
.usePlaintext()
.build()
private val blockingStub = MaestroDriverGrpc.newBlockingStub(channel)
private val asyncStub = MaestroDriverGrpc.newStub(channel)
private val documentBuilderFactory = DocumentBuilderFactory.newInstance()

private var instrumentationSession: AdbShellStream? = null
Expand Down Expand Up @@ -569,6 +549,55 @@ class AndroidDriver(
}
}

override fun addMedia(mediaFiles: List<File>) {
LOGGER.info("[Start] Adding media files")
mediaFiles.forEach { addMediaToDevice(it) }
LOGGER.info("[Done] Adding media files")
}

private fun addMediaToDevice(mediaFile: File) {
val namedSource = NamedSource(
mediaFile.name,
mediaFile.source(),
mediaFile.extension,
mediaFile.path
)
val responseObserver = BlockingStreamObserver<MaestroAndroid.AddMediaResponse>()
val requestStream = asyncStub.addMedia(responseObserver)
val ext =
MediaExt.values().firstOrNull { it.extName == namedSource.extension } ?: throw IllegalArgumentException(
"Extension .${namedSource.extension} is not yet supported for add media"
)

val buffer = Buffer()
val source = namedSource.source
while (source.read(buffer, CHUNK_SIZE) != -1L) {
requestStream.onNext(
addMediaRequest {
this.payload = payload {
data = ByteString.copyFrom(buffer.readByteArray())
}
this.mediaName = namedSource.name
this.mediaExt = ext.extName
}
)
buffer.clear()
}
source.close()
requestStream.onCompleted()
responseObserver.awaitResult()
}

override fun removeMedia() {
runCatching {
LOGGER.info("[Start] Removing media files")
dadb.shell("rm -rf /sdcard/Pictures/*")
dadb.shell("rm -rf /sdcard/Movies/*")
dadb.shell("rm -rf /sdcard/Music/*")
LOGGER.info("[Done] Removing media files")
}
}

private fun setAllPermissions(appId: String, permissionValue: String) {
val permissionsResult = runCatching {
val apkFile = AndroidAppFiles.getApkFile(dadb, appId)
Expand Down Expand Up @@ -826,5 +855,6 @@ class AndroidDriver(
private val PORT_TO_FORWARDER = mutableMapOf<Int, AutoCloseable>()
private val PORT_TO_ALLOCATION_POINT = mutableMapOf<Int, String>()
private const val SCREENSHOT_DIFF_THRESHOLD = 0.005
private const val CHUNK_SIZE = 1024L * 1024L * 3L
}
}
Loading

0 comments on commit faa32a6

Please sign in to comment.