-
Notifications
You must be signed in to change notification settings - Fork 296
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 new assertVisual
command
#2078
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -423,6 +423,25 @@ data class AssertWithAICommand( | |
} | ||
} | ||
|
||
data class AssertVisualCommand( | ||
val baseline: String, | ||
val thresholdPercentage: Int, | ||
override val optional: Boolean = false, | ||
override val label: String? = null, | ||
) : Command { | ||
override fun description(): String { | ||
return label ?: "Assert visual difference with baseline $baseline (threshold: $thresholdPercentage%)" | ||
} | ||
|
||
override fun evaluateScripts(jsEngine: JsEngine): Command { | ||
return copy( | ||
baseline = baseline.evaluateScripts(jsEngine) | ||
) | ||
} | ||
} | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add an integration test for this command for the different cases you expect? |
||
|
||
data class InputTextCommand( | ||
val text: String, | ||
override val label: String? = null, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,8 @@ | |
|
||
package maestro.orchestra | ||
|
||
import com.github.romankh3.image.comparison.ImageComparison | ||
import com.github.romankh3.image.comparison.model.ImageComparisonState | ||
import kotlinx.coroutines.runBlocking | ||
import maestro.* | ||
import maestro.Filters.asFilter | ||
|
@@ -46,8 +48,15 @@ import okio.Buffer | |
import okio.Sink | ||
import okio.buffer | ||
import okio.sink | ||
import java.awt.image.BufferedImage | ||
import java.io.File | ||
import java.io.IOException | ||
import java.lang.Long.max | ||
import java.nio.file.Files | ||
import java.nio.file.Paths | ||
import java.time.LocalDateTime | ||
import java.time.format.DateTimeFormatter | ||
import javax.imageio.ImageIO | ||
|
||
// TODO(bartkepacia): Use this in onCommandGeneratedOutput. | ||
// Caveat: | ||
|
@@ -267,6 +276,7 @@ class Orchestra( | |
is PasteTextCommand -> pasteText() | ||
is SwipeCommand -> swipeCommand(command) | ||
is AssertCommand -> assertCommand(command) | ||
is AssertVisualCommand -> assertVisualCommand(command) | ||
is AssertConditionCommand -> assertConditionCommand(command) | ||
is AssertNoDefectsWithAICommand -> assertNoDefectsWithAICommand(command) | ||
is AssertWithAICommand -> assertWithAICommand(command) | ||
|
@@ -406,6 +416,51 @@ class Orchestra( | |
false | ||
} | ||
|
||
private fun assertVisualCommand(command: AssertVisualCommand): Boolean { | ||
val baseline = command.baseline + ".png" | ||
val thresholdDifferencePercentage = (100 - command.thresholdPercentage).toDouble() | ||
|
||
val screenshotsDir = File(".maestro/visual_regression").apply { mkdirs() } | ||
|
||
val actual = File(screenshotsDir, baseline) | ||
|
||
val expected = File | ||
.createTempFile("screenshot-${System.currentTimeMillis()}", ".png") | ||
.also { it.deleteOnExit() } | ||
|
||
maestro.takeScreenshot(expected.sink(), false) | ||
|
||
if (!actual.exists()) { | ||
expected.copyTo(actual, overwrite = true) | ||
return true | ||
} | ||
|
||
val photoNow: BufferedImage = ImageIO.read(expected) | ||
val oldPhoto: BufferedImage = ImageIO.read(actual) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel we should ignore the system status bar as scope of this PR since it would start to fail with difference in time or having notifications 🤔 ? WDYT? |
||
val failedRegressionDir = File(".maestro/failed_visual_regression").apply { mkdirs() } | ||
val regressionFailedFile = File(failedRegressionDir, baseline) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this generated file highlight the diff between old and new? |
||
val comparison = | ||
ImageComparison(photoNow, oldPhoto, regressionFailedFile) | ||
|
||
comparison.apply { | ||
allowingPercentOfDifferentPixels = thresholdDifferencePercentage | ||
rectangleLineWidth = 10 | ||
pixelToleranceLevel = 50.00 | ||
minimalRectangleSize = 40 | ||
} | ||
|
||
val comparisonState = comparison.compareImages() | ||
|
||
if (ImageComparisonState.MISMATCH === comparisonState.imageComparisonState) { | ||
throw MaestroException.AssertionFailure( | ||
message = "Comparison error: ${command.description()} - threshold not met, current: ${100 - comparisonState.differencePercent}", | ||
hierarchyRoot = maestro.viewHierarchy().root, | ||
) | ||
} | ||
return true | ||
} | ||
|
||
|
||
private fun evalScriptCommand(command: EvalScriptCommand): Boolean { | ||
command.scriptString.evaluateScripts(jsEngine) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package maestro.orchestra.yaml | ||
|
||
import com.fasterxml.jackson.annotation.JsonCreator | ||
import java.lang.UnsupportedOperationException | ||
|
||
private const val DEFAULT_DIFF_THRESHOLD = 95 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be exposed as an environment variable (assuming you don't want to go as far as to expose it to workspace config at this point)? |
||
|
||
data class YamlAssertVisual( | ||
val baseline: String, | ||
val thresholdPercentage: Int = DEFAULT_DIFF_THRESHOLD, | ||
val label: String? = null, | ||
val optional: Boolean = false, | ||
) { | ||
|
||
companion object { | ||
@JvmStatic | ||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING) | ||
fun parse(baseline: String): YamlAssertVisual { | ||
return YamlAssertVisual( | ||
baseline = baseline | ||
) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a nitpick, I know, but thinking of terminal in a vscode window on a MacBook... Could this be shorter?
Perhaps not 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideas:
visual difference
->visual diff
maybe?