diff --git a/sample/android/screenshots/io.github.usefulness.testing.screenshot.sample.ScreenshotsSampleActivityTest_testScreenshotEntireActivity.png b/sample/android/screenshots/io.github.usefulness.testing.screenshot.sample.ScreenshotsSampleActivityTest_testScreenshotEntireActivity.png index 94f5b4af..984325ce 100644 Binary files a/sample/android/screenshots/io.github.usefulness.testing.screenshot.sample.ScreenshotsSampleActivityTest_testScreenshotEntireActivity.png and b/sample/android/screenshots/io.github.usefulness.testing.screenshot.sample.ScreenshotsSampleActivityTest_testScreenshotEntireActivity.png differ diff --git a/sample/android/screenshots/io.github.usefulness.testing.screenshot.sample.ScreenshotsSampleActivityTest_testScreenshotEntireActivityWithoutAccessibilityMetadata.png b/sample/android/screenshots/io.github.usefulness.testing.screenshot.sample.ScreenshotsSampleActivityTest_testScreenshotEntireActivityWithoutAccessibilityMetadata.png index 94f5b4af..984325ce 100644 Binary files a/sample/android/screenshots/io.github.usefulness.testing.screenshot.sample.ScreenshotsSampleActivityTest_testScreenshotEntireActivityWithoutAccessibilityMetadata.png and b/sample/android/screenshots/io.github.usefulness.testing.screenshot.sample.ScreenshotsSampleActivityTest_testScreenshotEntireActivityWithoutAccessibilityMetadata.png differ diff --git a/screenshot-testing-core/api/screenshot-testing-core.api b/screenshot-testing-core/api/screenshot-testing-core.api index 2984c7a7..f9c12b31 100644 --- a/screenshot-testing-core/api/screenshot-testing-core.api +++ b/screenshot-testing-core/api/screenshot-testing-core.api @@ -18,8 +18,25 @@ public abstract interface class io/github/usefulness/testing/screenshot/RecordBu public final class io/github/usefulness/testing/screenshot/Screenshot { public static final field INSTANCE Lio/github/usefulness/testing/screenshot/Screenshot; public static final field MAX_PIXELS J + public final fun getDefaultConfig ()Lio/github/usefulness/testing/screenshot/ScreenshotConfig; + public final fun setDefaultConfig (Lio/github/usefulness/testing/screenshot/ScreenshotConfig;)V public static final fun snap (Landroid/app/Activity;)Lio/github/usefulness/testing/screenshot/RecordBuilder; + public static final fun snap (Landroid/app/Activity;Lio/github/usefulness/testing/screenshot/ScreenshotConfig;)Lio/github/usefulness/testing/screenshot/RecordBuilder; public static final fun snap (Landroid/view/View;)Lio/github/usefulness/testing/screenshot/RecordBuilder; + public static final fun snap (Landroid/view/View;Lio/github/usefulness/testing/screenshot/ScreenshotConfig;)Lio/github/usefulness/testing/screenshot/RecordBuilder; + public static synthetic fun snap$default (Landroid/app/Activity;Lio/github/usefulness/testing/screenshot/ScreenshotConfig;ILjava/lang/Object;)Lio/github/usefulness/testing/screenshot/RecordBuilder; + public static synthetic fun snap$default (Landroid/view/View;Lio/github/usefulness/testing/screenshot/ScreenshotConfig;ILjava/lang/Object;)Lio/github/usefulness/testing/screenshot/RecordBuilder; +} + +public final class io/github/usefulness/testing/screenshot/ScreenshotConfig { + public fun ()V + public fun (IJ)V + public synthetic fun (IJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getMaxPixels ()J + public final fun getTileSize ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/github/usefulness/testing/screenshot/ScreenshotRunner { diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/RecordBuilder.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/RecordBuilder.kt index 9715efb6..cbd6f5a8 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/RecordBuilder.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/RecordBuilder.kt @@ -53,7 +53,7 @@ interface RecordBuilder { * Set the maximum number of pixels this screenshot should produce. Producing any number higher * will throw an exception. * - * @param maxPixels Maximum number of pixels this screenshot should produce. Specify zero or a + * [maxPixels] Maximum number of pixels this screenshot should produce. Specify zero or a * negative number for no limit. */ fun setMaxPixels(maxPixels: Long): RecordBuilder diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/Screenshot.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/Screenshot.kt index eec9836b..f8a26542 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/Screenshot.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/Screenshot.kt @@ -15,25 +15,31 @@ import io.github.usefulness.testing.screenshot.internal.ScreenshotImpl */ object Screenshot { + var defaultConfig = ScreenshotConfig() + /** - * Take a snapshot of an already measured and layout-ed view. See adb-logcat for how to pull the - * screenshot. + * Take a snapshot of an already measured and layout-ed view. * * * This method is thread safe. */ @JvmStatic - fun snap(measuredView: View): RecordBuilder = ScreenshotImpl.snap(measuredView) + @JvmOverloads + fun snap(measuredView: View, config: ScreenshotConfig = defaultConfig): RecordBuilder = + ScreenshotImpl(config = config).snap(measuredView) /** - * Take a snapshot of the activity and store it with the the testName. See the adb-logcat for how - * to pull the screenshot. + * Take a snapshot of the activity and store it with the the testName. * * * This method is thread safe. */ @JvmStatic - fun snap(activity: Activity): RecordBuilder = ScreenshotImpl.snapActivity(activity) + @JvmOverloads + fun snap(activity: Activity, config: ScreenshotConfig = defaultConfig): RecordBuilder = + ScreenshotImpl(config = config).snapActivity(activity) - const val MAX_PIXELS = ScreenshotImpl.MAX_PIXELS + @JvmField + @Deprecated("Scheduled for removal in next major release") + val MAX_PIXELS = ScreenshotConfig().maxPixels } diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/ScreenshotConfig.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/ScreenshotConfig.kt new file mode 100644 index 00000000..ed9c6e95 --- /dev/null +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/ScreenshotConfig.kt @@ -0,0 +1,16 @@ +package io.github.usefulness.testing.screenshot + +import androidx.annotation.Px +import io.github.usefulness.testing.screenshot.internal.Poko + +@Poko +class ScreenshotConfig( + @Px val tileSize: Int = 1536, + /** + * Set the maximum number of pixels this screenshot should produce. Producing any number higher + * will throw an exception. + * + * Maximum number of pixels this screenshot should produce. Specify zero or a negative number for no limit. + */ + @Px val maxPixels: Long = 10000000L, +) diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/ScreenshotRunner.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/ScreenshotRunner.kt index 5a1bbfb7..5301d291 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/ScreenshotRunner.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/ScreenshotRunner.kt @@ -2,7 +2,7 @@ package io.github.usefulness.testing.screenshot import android.app.Instrumentation import android.os.Bundle -import io.github.usefulness.testing.screenshot.internal.ScreenshotImpl +import io.github.usefulness.testing.screenshot.internal.MetadataRecorder /** * The ScreenshotRunner needs to be called from the top level Instrumentation test runner before and @@ -10,6 +10,7 @@ import io.github.usefulness.testing.screenshot.internal.ScreenshotImpl */ object ScreenshotRunner { + /** * Call this exactly once in your process before any screenshots are generated. * @@ -24,6 +25,6 @@ object ScreenshotRunner { * Typically this can be in `AndroidJUnitRunner#finish()` */ fun onDestroy() { - ScreenshotImpl.flush() + MetadataRecorder.flush() } } diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/TestNameDetector.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/TestNameDetector.kt index 12c2db7e..7005f7df 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/TestNameDetector.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/TestNameDetector.kt @@ -10,8 +10,8 @@ class TestMethodInfo( ) object TestNameDetector { - - private const val JUNIT_TEST = "org.junit.Test" + private const val JUNIT4_TEST = "org.junit.Test" + private const val JUNIT5_TEST = "org.junit.jupiter.api.Test" @JvmStatic @Suppress("ThrowingExceptionsWithoutMessageOrCause") @@ -42,6 +42,8 @@ object TestNameDetector { } } - private fun isTestMethod(method: Method) = - method.annotations.any { it.annotationClass.qualifiedName.equals(JUNIT_TEST, ignoreCase = true) } + private fun isTestMethod(method: Method) = method.annotations.any { + val qualifiedName = it.annotationClass.qualifiedName + qualifiedName.equals(JUNIT5_TEST, ignoreCase = true) || qualifiedName.equals(JUNIT4_TEST, ignoreCase = true) + } } diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/Album.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/Album.kt index 8a5a0d10..4ae49f7d 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/Album.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/Album.kt @@ -12,11 +12,6 @@ internal interface Album { */ fun writeBitmap(name: String, tilei: Int, tilej: Int, bitmap: Bitmap): String - /** - * Call after all the screenshots are done. - */ - fun flush() - /** * Opens a stream to dump the view hierarchy into. This should be called before addRecord() is * called for the given name. diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/AlbumImpl.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/AlbumImpl.kt index aaee4414..a47b482e 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/AlbumImpl.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/AlbumImpl.kt @@ -2,17 +2,12 @@ package io.github.usefulness.testing.screenshot.internal import android.graphics.Bitmap -internal class AlbumImpl(private val screenshotDirectories: ScreenshotDirectories) : Album { - private val metadataRecorder = MetadataRecorder(screenshotDirectories) - - override fun flush() { - metadataRecorder.flush() - } +internal class AlbumImpl : Album { override fun writeBitmap(name: String, tilei: Int, tilej: Int, bitmap: Bitmap): String { val tileName = generateTileName(name, tilei, tilej) val filename = getScreenshotFilenameInternal(tileName) - screenshotDirectories.openOutputFile(filename).use { + ScreenshotDirectories.openOutputFile(filename).use { bitmap.compress(Bitmap.CompressFormat.PNG, COMPRESSION_QUALITY, it) } return tileName @@ -27,13 +22,13 @@ internal class AlbumImpl(private val screenshotDirectories: ScreenshotDirectorie } private fun writeMetadataFile(name: String, data: String) { - screenshotDirectories.openOutputFile(name) + ScreenshotDirectories.openOutputFile(name) .use { it.write(data.toByteArray()) } } override fun addRecord(recordBuilder: RecordBuilderImpl) { recordBuilder.checkState() - if (metadataRecorder.snapshot().any { it.name == recordBuilder.name }) { + if (MetadataRecorder.snapshot().any { it.name == recordBuilder.name }) { if (recordBuilder.hasExplicitName()) { error("Can't create multiple screenshots with the same name: ${recordBuilder.name}") } @@ -43,7 +38,7 @@ internal class AlbumImpl(private val screenshotDirectories: ScreenshotDirectorie val tiling = recordBuilder.tiling - metadataRecorder.addNew( + MetadataRecorder.addNew( screenshot = MetadataRecorder.ScreenshotMetadata( description = recordBuilder.description, name = recordBuilder.name, @@ -60,10 +55,9 @@ internal class AlbumImpl(private val screenshotDirectories: ScreenshotDirectorie ) } - internal companion object { - private const val COMPRESSION_QUALITY = 90 + companion object { - fun create(): AlbumImpl = AlbumImpl(ScreenshotDirectories()) + private const val COMPRESSION_QUALITY = 90 /** * For a given screenshot, and a tile position, generates a name where we store the screenshot in diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/MetadataRecorder.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/MetadataRecorder.kt index 426d8f44..6f241486 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/MetadataRecorder.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/MetadataRecorder.kt @@ -8,11 +8,11 @@ import kotlinx.serialization.json.encodeToStream import java.io.FileNotFoundException @OptIn(ExperimentalSerializationApi::class) -internal class MetadataRecorder(private val screenshotDirectories: ScreenshotDirectories) { - private val metadataFileName = "metadata.json" +internal object MetadataRecorder { + private const val metadataFileName = "metadata.json" private val metadata by lazy { try { - screenshotDirectories.openInputFile(metadataFileName).use { metadataFile -> + ScreenshotDirectories.openInputFile(metadataFileName).use { metadataFile -> Json.decodeFromStream>(metadataFile).toMutableList() } } catch (ignored: FileNotFoundException) { @@ -23,7 +23,7 @@ internal class MetadataRecorder(private val screenshotDirectories: ScreenshotDir fun snapshot() = metadata.asSequence() fun flush() { - screenshotDirectories.openOutputFile(metadataFileName).use { output -> + ScreenshotDirectories.openOutputFile(metadataFileName).use { output -> Json.encodeToStream>(value = metadata, stream = output) } } diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/RecordBuilderImpl.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/RecordBuilderImpl.kt index fa482004..9e66ad76 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/RecordBuilderImpl.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/RecordBuilderImpl.kt @@ -3,10 +3,14 @@ package io.github.usefulness.testing.screenshot.internal import android.graphics.Bitmap import android.view.View import io.github.usefulness.testing.screenshot.RecordBuilder +import io.github.usefulness.testing.screenshot.ScreenshotConfig import java.io.File import java.nio.charset.Charset -internal class RecordBuilderImpl(private val mScreenshotImpl: ScreenshotImpl) : RecordBuilder { +internal class RecordBuilderImpl( + private val mScreenshotImpl: ScreenshotImpl, + config: ScreenshotConfig, +) : RecordBuilder { private val mExtras: MutableMap = HashMap() var description: String? = null private set @@ -33,7 +37,7 @@ internal class RecordBuilderImpl(private val mScreenshotImpl: ScreenshotImpl) : /** * @return The maximum number of pixels that is expected to be produced by this screenshot */ - var maxPixels: Long = DEFAULT_MAX_PIXELS + var maxPixels: Long = config.maxPixels private set override fun setDescription(description: String): RecordBuilderImpl { @@ -72,7 +76,7 @@ internal class RecordBuilderImpl(private val mScreenshotImpl: ScreenshotImpl) : return this } - override val bitmap: Bitmap? + override val bitmap: Bitmap get() = mScreenshotImpl.getBitmap(this) override fun setMaxPixels(maxPixels: Long): RecordBuilderImpl { diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/ScreenshotDirectories.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/ScreenshotDirectories.kt index 74533494..e250d7ba 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/ScreenshotDirectories.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/ScreenshotDirectories.kt @@ -7,7 +7,7 @@ import java.io.InputStream import java.io.OutputStream @OptIn(ExperimentalTestApi::class) -internal class ScreenshotDirectories { +internal object ScreenshotDirectories { fun openOutputFile(name: String): OutputStream = PlatformTestStorageRegistry.getInstance().openOutputFile(name) diff --git a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/ScreenshotImpl.kt b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/ScreenshotImpl.kt index 9a0d0c74..155130f6 100644 --- a/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/ScreenshotImpl.kt +++ b/screenshot-testing-core/src/main/kotlin/io/github/usefulness/testing/screenshot/internal/ScreenshotImpl.kt @@ -8,25 +8,25 @@ import android.graphics.PorterDuff import android.os.Handler import android.os.Looper import android.view.View +import io.github.usefulness.testing.screenshot.ScreenshotConfig import io.github.usefulness.testing.screenshot.TestNameDetector.getTestMethodInfo import io.github.usefulness.testing.screenshot.WindowAttachment.dispatchAttach +import io.github.usefulness.testing.screenshot.layouthierarchy.LayoutHierarchyDumper import io.github.usefulness.testing.screenshot.layouthierarchy.internal.AccessibilityHierarchyDumper.dumpHierarchy import io.github.usefulness.testing.screenshot.layouthierarchy.internal.AccessibilityIssuesDumper.dumpIssues import io.github.usefulness.testing.screenshot.layouthierarchy.internal.AccessibilityUtil.generateAccessibilityTree -import io.github.usefulness.testing.screenshot.layouthierarchy.LayoutHierarchyDumper import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.completeWith import kotlinx.coroutines.runBlocking import org.json.JSONObject import kotlin.math.min -internal object ScreenshotImpl { - private val mAlbum: Album = AlbumImpl.create() - - private const val TILE_SIZE = 512 +internal class ScreenshotImpl(private val config: ScreenshotConfig) { + private val mAlbum: Album = AlbumImpl() + private val tileSize = config.tileSize private val mBitmap by lazy { - Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Bitmap.Config.ARGB_8888) + Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888) } /** @@ -50,16 +50,12 @@ internal object ScreenshotImpl { fun snap(measuredView: View): RecordBuilderImpl { val testMethodInfo = getTestMethodInfo() - return RecordBuilderImpl(this) + return RecordBuilderImpl(this, config = config) .setView(measuredView) .setTestClass(testMethodInfo?.className ?: "unknown") .setTestName(testMethodInfo?.methodName ?: "unknown") } - fun flush() { - mAlbum.flush() - } - private fun storeBitmap(recordBuilder: RecordBuilderImpl) { if (recordBuilder.tiling.getAt(0, 0) != null || recordBuilder.error != null) { return @@ -82,8 +78,8 @@ internal object ScreenshotImpl { assertNotTooLarge(width, height, recordBuilder) - val maxi = (width + TILE_SIZE - 1) / TILE_SIZE - val maxj = (height + TILE_SIZE - 1) / TILE_SIZE + val maxi = (width + tileSize - 1) / tileSize + val maxj = (height + tileSize - 1) / tileSize recordBuilder.setTiling(Tiling(maxi, maxj)) for (i in 0 until maxi) { @@ -99,10 +95,10 @@ internal object ScreenshotImpl { private fun drawTile(measuredView: View, i: Int, j: Int, recordBuilder: RecordBuilderImpl) { val width = measuredView.width val height = measuredView.height - val left = i * TILE_SIZE - val top = j * TILE_SIZE - val right = min((left + TILE_SIZE).toDouble(), width.toDouble()).toInt() - val bottom = min((top + TILE_SIZE).toDouble(), height.toDouble()).toInt() + val left = i * tileSize + val top = j * tileSize + val right = min((left + tileSize).toDouble(), width.toDouble()).toInt() + val bottom = min((top + tileSize).toDouble(), height.toDouble()).toInt() mBitmap.reconfigure(right - left, bottom - top, Bitmap.Config.ARGB_8888) val canvas = Canvas(mBitmap) @@ -110,7 +106,7 @@ internal object ScreenshotImpl { drawClippedView(measuredView, left, top, canvas) val tempName = mAlbum.writeBitmap(recordBuilder.name, i, j, mBitmap) - recordBuilder.tiling.setAt(left / TILE_SIZE, top / TILE_SIZE, tempName) + recordBuilder.tiling.setAt(left / tileSize, top / tileSize, tempName) } private fun clearCanvas(canvas: Canvas) { @@ -176,34 +172,35 @@ internal object ScreenshotImpl { return bmp } - private val isUiThread: Boolean - get() = Looper.getMainLooper().thread === Thread.currentThread() + companion object { - private fun runCallableOnUiThread(callable: () -> T): T { - val completableDeferred = CompletableDeferred() - val handler = Handler(Looper.getMainLooper()) + private val isUiThread: Boolean + get() = Looper.getMainLooper().thread === Thread.currentThread() - handler.post { - completableDeferred.completeWith(runCatching(callable)) - } + private fun runCallableOnUiThread(callable: () -> T): T { + val completableDeferred = CompletableDeferred() + val handler = Handler(Looper.getMainLooper()) - return runBlocking { completableDeferred.await() } - } + handler.post { + completableDeferred.completeWith(runCatching(callable)) + } - /** - * The version of the metadata file generated. This should be bumped whenever the structure of the - * metadata file changes in such a way that would cause a comparison between old and new files to - * be invalid or not useful. - */ - private const val METADATA_VERSION = 1 + return runBlocking { completableDeferred.await() } + } - private fun assertNotTooLarge(width: Int, height: Int, recordBuilder: RecordBuilderImpl) { - val maxPixels = recordBuilder.maxPixels - if (maxPixels <= 0) { - return + private fun assertNotTooLarge(width: Int, height: Int, recordBuilder: RecordBuilderImpl) { + val maxPixels = recordBuilder.maxPixels + if (maxPixels <= 0) { + return + } + check((width.toLong()) * height <= maxPixels) { "View too large: ($width, $height)" } } - check((width.toLong()) * height <= maxPixels) { "View too large: ($width, $height)" } - } - const val MAX_PIXELS = RecordBuilderImpl.DEFAULT_MAX_PIXELS + /** + * The version of the metadata file generated. This should be bumped whenever the structure of the + * metadata file changes in such a way that would cause a comparison between old and new files to + * be invalid or not useful. + */ + private const val METADATA_VERSION = 1 + } } diff --git a/screenshot-testing-plugin/src/main/kotlin/io/github/usefulness/testing/screenshot/verification/Recorder.kt b/screenshot-testing-plugin/src/main/kotlin/io/github/usefulness/testing/screenshot/verification/Recorder.kt index e2fed5cc..3d61151b 100644 --- a/screenshot-testing-plugin/src/main/kotlin/io/github/usefulness/testing/screenshot/verification/Recorder.kt +++ b/screenshot-testing-plugin/src/main/kotlin/io/github/usefulness/testing/screenshot/verification/Recorder.kt @@ -2,6 +2,7 @@ package io.github.usefulness.testing.screenshot.verification import com.sksamuel.scrimage.Dimension import com.sksamuel.scrimage.ImmutableImage +import com.sksamuel.scrimage.canvas.drawables.Rect import com.sksamuel.scrimage.color.Colors import com.sksamuel.scrimage.composite.DifferenceComposite import com.sksamuel.scrimage.composite.RedComposite @@ -9,6 +10,7 @@ import com.sksamuel.scrimage.nio.PngWriter import com.sksamuel.scrimage.pixels.Pixel import io.github.usefulness.testing.screenshot.verification.MetadataParser.ScreenshotMetadata import io.github.usefulness.testing.screenshot.verification.Recorder.VerificationResult.Mismatch +import java.awt.Color import java.awt.image.BufferedImage import java.io.File import kotlin.math.abs @@ -56,11 +58,13 @@ internal class Recorder( val inArgb = existing.copy(BufferedImage.TYPE_INT_ARGB) val outArgb = incoming.copy(BufferedImage.TYPE_INT_ARGB) val redDiff = inArgb.composite(RedComposite(1.0), outArgb) - val diffDiff = inArgb.composite(DifferenceComposite(0.9), outArgb) - existing.output(PngWriter.MaxCompression, failureDirectory.resolve("${key}_expected.png")) - incoming.output(PngWriter.MaxCompression, failureDirectory.resolve("${key}_actual.png")) - redDiff.output(PngWriter.MaxCompression, failureDirectory.resolve("${key}_diff_red.png")) - diffDiff.output(PngWriter.MaxCompression, failureDirectory.resolve("${key}_diff_diff.png")) + val diffDiff = inArgb.composite(DifferenceComposite(0.98), outArgb) + val redBorder = createRedBorder(existing = inArgb, incoming = outArgb) + existing.output(PngWriter.NoCompression, failureDirectory.resolve("${key}_expected.png")) + incoming.output(PngWriter.NoCompression, failureDirectory.resolve("${key}_actual.png")) + redDiff.output(PngWriter.NoCompression, failureDirectory.resolve("${key}_diff_red.png")) + diffDiff.output(PngWriter.NoCompression, failureDirectory.resolve("${key}_diff_diff.png")) + redBorder.output(PngWriter.NoCompression, failureDirectory.resolve("${key}_diff_border.png")) } } @@ -91,7 +95,7 @@ internal class Recorder( val newScreenshotPixels = other.pixels() if (oldScreenshotPixels.size != newScreenshotPixels.size) { - return Float.NaN + return Float.MAX_VALUE } val diff = List(newScreenshotPixels.size) { @@ -128,6 +132,32 @@ internal class Recorder( return sqrt(sumOfSquares.toFloat() / (imageDimensions.x * imageDimensions.y)) } + private fun createRedBorder(existing: ImmutableImage, incoming: ImmutableImage): ImmutableImage { + val oldScreenshotPixels = existing.pixels() + val newScreenshotPixels = incoming.pixels() + val differentPixels = List(newScreenshotPixels.size) { + val new = newScreenshotPixels[it] + val old = oldScreenshotPixels.getOrNull(it) + val difference = if (old == null) { + Int.MAX_VALUE + } else { + abs(new.argb - old.argb) + } + Pixel(new.x, new.y, difference) + } + .asSequence() + .filter { it.argb > 0 } + val (startX, startY) = differentPixels.minOf { it.x } to differentPixels.minOf { it.y } + val (endX, endY) = differentPixels.maxOf { it.x } to differentPixels.maxOf { it.y } + + return incoming.toCanvas().draw( + Rect(startX, startY, endX - startX, endY - startY) { g2 -> + g2.setBasicStroke(5f) + g2.color = Color.RED + }, + ).image + } + private fun loadRecordedImages() = metadata.associate { screenshot -> val tiles = List(screenshot.tileWidth) { x -> List(screenshot.tileHeight) { y ->