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

feat: add cropOn screenshots capability #2086

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
28 changes: 28 additions & 0 deletions maestro-client/src/main/java/maestro/Maestro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import maestro.drivers.WebDriver
import maestro.utils.MaestroTimer
import maestro.utils.ScreenshotUtils
import maestro.utils.SocketUtils
import okio.Buffer
import okio.Sink
import okio.buffer
import okio.sink
Expand All @@ -34,6 +35,7 @@ import org.slf4j.LoggerFactory
import java.awt.image.BufferedImage
import java.io.File
import java.util.*
import javax.imageio.ImageIO
import kotlin.system.measureTimeMillis

@Suppress("unused", "MemberVisibilityCanBePrivate")
Expand Down Expand Up @@ -530,6 +532,32 @@ class Maestro(
}
}

fun takePartialScreenshot(sink: Sink, bounds: Bounds, compressed: Boolean) {
LOGGER.info("Taking partial screenshot")
val (x, y, width, height) = bounds

val originalImage = Buffer().apply {
ScreenshotUtils.takeScreenshot(this, compressed, driver)
}.let { buffer ->
buffer.inputStream().use { ImageIO.read(it) }
}

val dpr = cachedDeviceInfo.run { heightPixels/heightGrid } // device pixel ratio

val cropWidth = (x + width).coerceAtMost(originalImage.width) - x
val cropHeight = (y + height).coerceAtMost(originalImage.height) - y

val croppedImage = originalImage.getSubimage(
x * dpr, y * dpr, cropWidth * dpr, cropHeight * dpr
)

sink
.buffer()
.use {
ImageIO.write(croppedImage, "png", it.outputStream())
}
}

fun startScreenRecording(out: Sink): ScreenRecording {
LOGGER.info("Starting screen recording")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,12 +573,17 @@ data class EraseTextCommand(

data class TakeScreenshotCommand(
val path: String,
val cropOn: ElementSelector? = null,
override val label: String? = null,
override val optional: Boolean = false,
) : Command {

override fun description(): String {
return label ?: "Take screenshot $path"
return label ?: if (cropOn != null) {
"Take screenshot $path, cropped to ${cropOn.description()}"
} else {
"Take screenshot $path"
}
}

override fun evaluateScripts(jsEngine: JsEngine): TakeScreenshotCommand {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ import okio.Buffer
import okio.Sink
import okio.buffer
import okio.sink
import java.awt.image.BufferedImage
import java.io.File
import java.lang.Long.max
import javax.imageio.ImageIO

// TODO(bartkepacia): Use this in onCommandGeneratedOutput.
// Caveat:
Expand Down Expand Up @@ -747,11 +749,16 @@ class Orchestra(

private fun takeScreenshotCommand(command: TakeScreenshotCommand): Boolean {
val pathStr = command.path + ".png"
val cropped = command.cropOn?.let { findElement(it, optional = command.optional) }
val file = screenshotsDir
?.let { File(it, pathStr) }
?: File(pathStr)

maestro.takeScreenshot(file, false)
if(cropped == null){
maestro.takeScreenshot(file.sink(), false)
}else{
maestro.takePartialScreenshot(sink = file.sink(), bounds = cropped.element.bounds, compressed = false)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
}
if (cropped == null){
maestro.takeScreenshot(file.sink(), false)
} else {
maestro.takePartialScreenshot(sink = file.sink(), bounds = cropped.element.bounds, compressed = false)
}

nit: White spaces.


return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,16 @@ data class YamlFluentCommand(
hideKeyboard != null -> listOf(MaestroCommand(HideKeyboardCommand(label = hideKeyboard.label, optional = hideKeyboard.optional)))
pasteText != null -> listOf(MaestroCommand(PasteTextCommand(label = pasteText.label, optional = pasteText.optional)))
scroll != null -> listOf(MaestroCommand(ScrollCommand(label = scroll.label, optional = scroll.optional)))
takeScreenshot != null -> listOf(MaestroCommand(TakeScreenshotCommand(path = takeScreenshot.path, label = takeScreenshot.label, optional = takeScreenshot.optional)))
takeScreenshot != null -> listOf(
MaestroCommand(
TakeScreenshotCommand(
path = takeScreenshot.path,
label = takeScreenshot.label,
optional = takeScreenshot.optional,
cropOn = takeScreenshot.cropOn?.let { toElementSelector(selectorUnion = it) })
)
)

extendedWaitUntil != null -> listOf(extendedWait(extendedWaitUntil))
stopApp != null -> listOf(
MaestroCommand(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonCreator
data class YamlTakeScreenshot(
val path: String,
val label: String? = null,
val cropOn: YamlElementSelectorUnion? = null,
val optional: Boolean = false,
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ internal class MaestroCommandSerializationTest {
fun `serialize TakeScreenshotCommand`() {
// given
val command = MaestroCommand(
TakeScreenshotCommand("screenshot.png")
TakeScreenshotCommand("screenshot.png", cropOn = ElementSelector(textRegex = "[A-f0-9]"))
)

// when
Expand All @@ -441,6 +441,10 @@ internal class MaestroCommandSerializationTest {
{
"takeScreenshotCommand" : {
"path" : "screenshot.png",
"cropOn" : {
"textRegex" : "[A-f0-9]",
"optional" : false
},
"optional" : false
}
}
Expand Down
39 changes: 38 additions & 1 deletion maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import java.io.File
import java.nio.file.Paths
import kotlin.system.measureTimeMillis
import maestro.orchestra.util.Env.withDefaultEnvVars
import javax.imageio.ImageIO

class IntegrationTest {

Expand Down Expand Up @@ -3135,7 +3136,6 @@ class IntegrationTest {
)
}

@Test
fun `Case 118 - Scroll until view is visible - no negative values allowed`() {
// Given
val commands = readCommands("118_scroll_until_visible_negative")
Expand Down Expand Up @@ -3171,6 +3171,43 @@ class IntegrationTest {
)
}

@Test
fun `Case 119 - Take cropped screenshot`() {
// Given
val commands = readCommands("119_take_cropped_screenshot")
val boundHeight = 100
val boundWidth = 100

val driver = driver {
element {
id = "element_id"
bounds = Bounds(0,0,boundHeight,boundWidth)
}
}

val device = driver.deviceInfo()
val dpr = device.heightPixels / device.heightGrid

// When
Maestro(driver).use {
orchestra(it).runFlow(commands)
}

// Then
// No test failure
driver.assertEvents(
listOf(
Event.TakeScreenshot,
)
)
val file = File("119_take_cropped_screenshot_with_filename.png")
val image = ImageIO.read(file)

assert(file.exists())
assert(image.width == (boundWidth * dpr))
assert(image.height == (boundHeight * dpr))
}

private fun orchestra(
maestro: Maestro,
) = Orchestra(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
appId: com.example.app
---
- takeScreenshot:
path: ${MAESTRO_FILENAME}_with_filename
cropOn:
id: element_id
Loading