From 58de6559add0d6786f6b318ea21502520ce116ef Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Tue, 12 Sep 2023 15:02:49 -0400 Subject: [PATCH 01/13] 41: Refactor compose method, compare method and findRootView --- CHANGELOG.md | 8 ++++++++ .../java/dev/testify/internal/logic/TakeScreenshot.kt | 2 -- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36631067..875e208b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ ### Library +#### Changed + +- `ScreenshotRule.getRootView()` is now an extension function `fun Activity.findRootView(@IdRes rootViewId: Int): ViewGroup` +- `ScreenshotRule.setCaptureMethod()` is deprecated. Use `var captureMethod: CaptureMethod?` on `TestifyConfiguration` to set the capture method. +- `ScreenshotRule.setCompareMethod()` is deprecated. Use `var compareMethod: CompareMethod?` on `TestifyConfiguration` to set the compare method. +- `ScreenshotRule.compareBitmaps()` is now a top-level function. +- `ScreenshotRule.takeScreenshot()` is now a top-level function. + #### Fixed - [#175](https://github.com/ndtp/android-testify/issues/175): Output from Gradle Managed Devices now named according to Testify naming strategy diff --git a/Library/src/main/java/dev/testify/internal/logic/TakeScreenshot.kt b/Library/src/main/java/dev/testify/internal/logic/TakeScreenshot.kt index c62ec11f..f4064a57 100644 --- a/Library/src/main/java/dev/testify/internal/logic/TakeScreenshot.kt +++ b/Library/src/main/java/dev/testify/internal/logic/TakeScreenshot.kt @@ -3,7 +3,6 @@ package dev.testify.internal.logic import android.app.Activity import android.graphics.Bitmap import android.view.View -import androidx.test.annotation.ExperimentalTestApi import dev.testify.CaptureMethod import dev.testify.createBitmapFromActivity @@ -18,7 +17,6 @@ import dev.testify.createBitmapFromActivity * @return A [Bitmap] representing the captured [screenshotView] in [activity] * Will return [null] if there is an error capturing the bitmap. */ -@ExperimentalTestApi fun takeScreenshot( activity: Activity, fileName: String, From 4228f473ca8effa28ba10491fe24eab419d58d96 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 08:48:39 -0400 Subject: [PATCH 02/13] 41: Extract ExtrasProvider --- .../main/java/dev/testify/ExtrasProvider.kt | 28 +++++++++++++++++++ .../main/java/dev/testify/ScreenshotRule.kt | 1 - 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Library/src/main/java/dev/testify/ExtrasProvider.kt diff --git a/Library/src/main/java/dev/testify/ExtrasProvider.kt b/Library/src/main/java/dev/testify/ExtrasProvider.kt new file mode 100644 index 00000000..15493f8e --- /dev/null +++ b/Library/src/main/java/dev/testify/ExtrasProvider.kt @@ -0,0 +1,28 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify + +import android.os.Bundle + +typealias ExtrasProvider = (bundle: Bundle) -> Unit diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index 80a289e7..6182e590 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -93,7 +93,6 @@ import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isR typealias ViewModification = (rootView: ViewGroup) -> Unit typealias ViewProvider = (rootView: ViewGroup) -> View -typealias ExtrasProvider = (bundle: Bundle) -> Unit @Suppress("unused", "MemberVisibilityCanBePrivate") open class ScreenshotRule @JvmOverloads constructor( From 680d94bf4d053ebc79eeb7c68ca707d2f42aef04 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 08:55:31 -0400 Subject: [PATCH 03/13] 41: Extract ViewModification --- .../main/java/dev/testify/ScreenshotRule.kt | 1 - .../main/java/dev/testify/ViewModification.kt | 28 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Library/src/main/java/dev/testify/ViewModification.kt diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index 6182e590..5dad27ed 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -91,7 +91,6 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isRecordMode as recordMode -typealias ViewModification = (rootView: ViewGroup) -> Unit typealias ViewProvider = (rootView: ViewGroup) -> View @Suppress("unused", "MemberVisibilityCanBePrivate") diff --git a/Library/src/main/java/dev/testify/ViewModification.kt b/Library/src/main/java/dev/testify/ViewModification.kt new file mode 100644 index 00000000..d5530fa1 --- /dev/null +++ b/Library/src/main/java/dev/testify/ViewModification.kt @@ -0,0 +1,28 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify + +import android.view.ViewGroup + +typealias ViewModification = (rootView: ViewGroup) -> Unit From 4f1c7f5f1323df2e71e30ce9b69d7d98528de285 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 08:58:32 -0400 Subject: [PATCH 04/13] 41: Extract ViewProvider --- .../main/java/dev/testify/ScreenshotRule.kt | 1 - .../src/main/java/dev/testify/ViewProvider.kt | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 Library/src/main/java/dev/testify/ViewProvider.kt diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index 5dad27ed..11df5e8b 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -91,7 +91,6 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isRecordMode as recordMode -typealias ViewProvider = (rootView: ViewGroup) -> View @Suppress("unused", "MemberVisibilityCanBePrivate") open class ScreenshotRule @JvmOverloads constructor( diff --git a/Library/src/main/java/dev/testify/ViewProvider.kt b/Library/src/main/java/dev/testify/ViewProvider.kt new file mode 100644 index 00000000..e4f18516 --- /dev/null +++ b/Library/src/main/java/dev/testify/ViewProvider.kt @@ -0,0 +1,29 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify + +import android.view.View +import android.view.ViewGroup + +typealias ViewProvider = (rootView: ViewGroup) -> View From 768e256747ed3ee61d1b8dd9c07a2153db1b0f53 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 12:47:12 -0400 Subject: [PATCH 05/13] 41: Extract isRunningOnUiThread --- .../main/java/dev/testify/ScreenshotRule.kt | 5 +--- .../internal/helpers/IsRunningOnUiThread.kt | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 Library/src/main/java/dev/testify/internal/helpers/IsRunningOnUiThread.kt diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index 11df5e8b..453d3f21 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -73,6 +73,7 @@ import dev.testify.internal.helpers.EspressoActions import dev.testify.internal.helpers.EspressoHelper import dev.testify.internal.helpers.ResourceWrapper import dev.testify.internal.helpers.findRootView +import dev.testify.internal.helpers.isRunningOnUiThread import dev.testify.internal.helpers.registerActivityProvider import dev.testify.internal.logic.compareBitmaps import dev.testify.internal.logic.takeScreenshot @@ -147,10 +148,6 @@ open class ScreenshotRule @JvmOverloads constructor( addScreenshotObserver(TestifyFeatures) } - private fun isRunningOnUiThread(): Boolean { - return Looper.getMainLooper().thread == Thread.currentThread() - } - fun setRootViewId(@IdRes rootViewId: Int): ScreenshotRule { this.rootViewId = rootViewId return this diff --git a/Library/src/main/java/dev/testify/internal/helpers/IsRunningOnUiThread.kt b/Library/src/main/java/dev/testify/internal/helpers/IsRunningOnUiThread.kt new file mode 100644 index 00000000..97f6a4df --- /dev/null +++ b/Library/src/main/java/dev/testify/internal/helpers/IsRunningOnUiThread.kt @@ -0,0 +1,29 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify.internal.helpers + +import android.os.Looper + +fun isRunningOnUiThread(): Boolean = + Looper.getMainLooper().thread == Thread.currentThread() From 76303449e76d4bbb066b30b31c206f7fa8c7627e Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 13:03:07 -0400 Subject: [PATCH 06/13] 41: Extract outputFileName() --- .../main/java/dev/testify/ScreenshotRule.kt | 11 +---- .../internal/helpers/OutputFileName.kt | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 Library/src/main/java/dev/testify/internal/helpers/OutputFileName.kt diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index 453d3f21..56083070 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -32,7 +32,6 @@ import android.content.Intent import android.graphics.Bitmap import android.os.Bundle import android.os.Debug -import android.os.Looper import android.view.View import android.view.ViewGroup import androidx.annotation.CallSuper @@ -46,7 +45,6 @@ import androidx.test.rule.ActivityTestRule import dev.testify.annotation.ScreenshotInstrumentation import dev.testify.annotation.TestifyLayout import dev.testify.internal.DEFAULT_FOLDER_FORMAT -import dev.testify.internal.DEFAULT_NAME_FORMAT import dev.testify.internal.DeviceStringFormatter import dev.testify.internal.ScreenshotRuleCompatibilityMethods import dev.testify.internal.TestifyConfiguration @@ -74,6 +72,7 @@ import dev.testify.internal.helpers.EspressoHelper import dev.testify.internal.helpers.ResourceWrapper import dev.testify.internal.helpers.findRootView import dev.testify.internal.helpers.isRunningOnUiThread +import dev.testify.internal.helpers.outputFileName import dev.testify.internal.helpers.registerActivityProvider import dev.testify.internal.logic.compareBitmaps import dev.testify.internal.logic.takeScreenshot @@ -427,13 +426,7 @@ open class ScreenshotRule @JvmOverloads constructor( try { val description = getInstrumentation().testDescription reporter?.captureOutput(this) - outputFileName = formatDeviceString( - DeviceStringFormatter( - testContext, - description.nameComponents - ), - DEFAULT_NAME_FORMAT - ) + outputFileName = testContext.outputFileName(description) screenshotLifecycleObservers.forEach { it.beforeInitializeView(activity) } initializeView(activity) diff --git a/Library/src/main/java/dev/testify/internal/helpers/OutputFileName.kt b/Library/src/main/java/dev/testify/internal/helpers/OutputFileName.kt new file mode 100644 index 00000000..b6483d95 --- /dev/null +++ b/Library/src/main/java/dev/testify/internal/helpers/OutputFileName.kt @@ -0,0 +1,43 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify.internal.helpers + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import dev.testify.TestDescription +import dev.testify.internal.DEFAULT_NAME_FORMAT +import dev.testify.internal.DeviceStringFormatter +import dev.testify.internal.formatDeviceString +import dev.testify.testDescription + +fun Context.outputFileName( + description: TestDescription = InstrumentationRegistry.getInstrumentation().testDescription, + format: String = DEFAULT_NAME_FORMAT +) = formatDeviceString( + formatter = DeviceStringFormatter( + context = this, + testName = description.nameComponents + ), + format = format +) From a654c7ad163d5c36fe9596fc445e874ec2c2ff30 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 13:15:52 -0400 Subject: [PATCH 07/13] 41: Remove ScreenshotRule.generateHighContrastDiff --- CHANGELOG.md | 9 +++++++++ Library/src/main/java/dev/testify/ScreenshotRule.kt | 8 ++++++-- .../testify/internal/processor/diff/HighContrastDiff.kt | 7 +++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875e208b..05ecb189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,15 @@ - `ScreenshotRule.compareBitmaps()` is now a top-level function. - `ScreenshotRule.takeScreenshot()` is now a top-level function. +#### Added + +- `isRunningOnUiThread()` added as a top-level function. +- `outputFileName()` added as an extension method for `Context`. + +#### Removed + +- `open fun ScreenshotRule.generateHighContrastDiff(baselineBitmap: Bitmap, currentBitmap: Bitmap)` has been removed. Use + #### Fixed - [#175](https://github.com/ndtp/android-testify/issues/175): Output from Gradle Managed Devices now named according to Testify naming strategy diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index 56083070..ede03608 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -29,7 +29,6 @@ package dev.testify import android.annotation.SuppressLint import android.app.Activity import android.content.Intent -import android.graphics.Bitmap import android.os.Bundle import android.os.Debug import android.view.View @@ -493,7 +492,12 @@ open class ScreenshotRule @JvmOverloads constructor( throw FinalizeDestinationException(destination.description) if (TestifyFeatures.GenerateDiffs.isEnabled(activity)) { - generateHighContrastDiff(baselineBitmap, currentBitmap) + HighContrastDiff(configuration.exclusionRects) + .name(outputFileName) + .baseline(baselineBitmap) + .current(currentBitmap) + .exactness(configuration.exactness) + .generate(context = activity) } if (isRecordMode || recordMode) { instrumentationPrintln( diff --git a/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt b/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt index 7be17131..52d0a110 100644 --- a/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt +++ b/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt @@ -34,6 +34,13 @@ import dev.testify.internal.processor.createBitmap import dev.testify.output.getDestination import dev.testify.saveBitmapToDestination +/** + * Given [baselineBitmap] and [currentBitmap], use [HighContrastDiff] to write a companion .diff image for the + * current test. + * + * This diff image is a high-contrast image where each difference, regardless of how minor, is indicated in red + * against a black background. + */ class HighContrastDiff(private val exclusionRects: Set) { private lateinit var fileName: String From d61f8455b9b92dfe99f1b3547947d85a10ac86bb Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 13:34:46 -0400 Subject: [PATCH 08/13] 41: Refactor annotation helpers --- CHANGELOG.md | 7 +++- .../main/java/dev/testify/ScreenshotRule.kt | 29 ++------------ .../annotation/AnnotationExtensions.kt | 40 ++++++++++++++++++- .../testify/internal/TestifyConfiguration.kt | 6 +-- .../InstrumentationRegistryExtensions.kt | 7 ---- 5 files changed, 50 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05ecb189..a0d0afbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,14 @@ #### Changed -- `ScreenshotRule.getRootView()` is now an extension function `fun Activity.findRootView(@IdRes rootViewId: Int): ViewGroup` +- `ScreenshotRule.getRootView()` is now an extension function `fun Activity.findRootView(@IdRes rootViewId: Int): ViewGroup`. - `ScreenshotRule.setCaptureMethod()` is deprecated. Use `var captureMethod: CaptureMethod?` on `TestifyConfiguration` to set the capture method. - `ScreenshotRule.setCompareMethod()` is deprecated. Use `var compareMethod: CompareMethod?` on `TestifyConfiguration` to set the compare method. - `ScreenshotRule.compareBitmaps()` is now a top-level function. - `ScreenshotRule.takeScreenshot()` is now a top-level function. +- `ScreenshotRule.getScreenshotInstrumentationAnnotation()` is now a top-level function. +- `Collection.getAnnotation()` renamed to `Collection.findAnnotation()`. +- Package for `getScreenshotAnnotationName()` changed from `dev.testify.internal.extensions` to `dev.testify.annotation`. #### Added @@ -19,7 +22,7 @@ #### Removed -- `open fun ScreenshotRule.generateHighContrastDiff(baselineBitmap: Bitmap, currentBitmap: Bitmap)` has been removed. Use +- `open fun ScreenshotRule.generateHighContrastDiff(baselineBitmap: Bitmap, currentBitmap: Bitmap)` has been removed. Use `class HighContrastDiff` directly. #### Fixed diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index ede03608..ebe9a63e 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -41,8 +41,10 @@ import androidx.annotation.VisibleForTesting import androidx.test.annotation.ExperimentalTestApi import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.ActivityTestRule -import dev.testify.annotation.ScreenshotInstrumentation import dev.testify.annotation.TestifyLayout +import dev.testify.annotation.findAnnotation +import dev.testify.annotation.getScreenshotAnnotationName +import dev.testify.annotation.getScreenshotInstrumentationAnnotation import dev.testify.internal.DEFAULT_FOLDER_FORMAT import dev.testify.internal.DeviceStringFormatter import dev.testify.internal.ScreenshotRuleCompatibilityMethods @@ -62,7 +64,6 @@ import dev.testify.internal.exception.ViewModificationException import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.getModuleName import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.instrumentationPrintln import dev.testify.internal.extensions.cyan -import dev.testify.internal.extensions.getScreenshotAnnotationName import dev.testify.internal.extensions.isInvokedFromPlugin import dev.testify.internal.formatDeviceString import dev.testify.internal.helpers.ActivityProvider @@ -264,18 +265,10 @@ open class ScreenshotRule @JvmOverloads constructor( ) reporter?.startTest(getInstrumentation().testDescription) - val testifyLayout = methodAnnotations?.getAnnotation() + val testifyLayout = methodAnnotations?.findAnnotation() targetLayoutId = testifyLayout?.resolvedLayoutId ?: View.NO_ID } - private inline fun Collection.getAnnotation(): T? { - return this.find { it is T } as? T - } - - private inline fun Collection.getAnnotation(name: String): T? { - return this.find { it.annotationClass.qualifiedName == name } as? T - } - @get:LayoutRes private val TestifyLayout.resolvedLayoutId: Int @SuppressLint("DiscouragedApi") @@ -287,20 +280,6 @@ open class ScreenshotRule @JvmOverloads constructor( return layoutId } - /** - * Get the [ScreenshotInstrumentation] instance associated with the test method - * - * @param classAnnotations - A [List] of all the [Annotation]s defined on the currently running test class - * @param methodAnnotations - A [Collection] of all the [Annotation]s defined on the currently running test method - */ - internal fun getScreenshotInstrumentationAnnotation( - classAnnotations: List, - methodAnnotations: Collection? - ): Annotation? { - val annotationName = getScreenshotAnnotationName() - return classAnnotations.getAnnotation(annotationName) ?: methodAnnotations?.getAnnotation(annotationName) - } - /** * Assert that the @ScreenshotInstrumentation is defined on the test method. * diff --git a/Library/src/main/java/dev/testify/annotation/AnnotationExtensions.kt b/Library/src/main/java/dev/testify/annotation/AnnotationExtensions.kt index 7879fc5c..87afc809 100644 --- a/Library/src/main/java/dev/testify/annotation/AnnotationExtensions.kt +++ b/Library/src/main/java/dev/testify/annotation/AnnotationExtensions.kt @@ -24,6 +24,42 @@ package dev.testify.annotation -inline fun Collection.getAnnotation(): T? { - return this.find { it is T } as? T +import androidx.test.platform.app.InstrumentationRegistry + +/** + * Returns the fully qualified dot-separated name of the annotation required by the Gradle plugin. + */ +fun getScreenshotAnnotationName(): String = + InstrumentationRegistry.getArguments().getString("annotation", ScreenshotInstrumentation::class.qualifiedName) + +/** + * Find the first [Annotation] in the given [Collection] which is of type [T] + * + * @return Annotation of type T + */ +inline fun Collection.findAnnotation(): T? = + this.find { it is T } as? T + +/** + * Find the first [Annotation] in the given [Collection] which has the given [name] + * + * @param name - The qualified class name of the requested annotation + * + * @return Annotation of type T + */ +inline fun Collection.findAnnotation(name: String): T? = + this.find { it.annotationClass.qualifiedName == name } as? T + +/** + * Get the [ScreenshotInstrumentation] instance associated with the test method + * + * @param classAnnotations - A [List] of all the [Annotation]s defined on the currently running test class + * @param methodAnnotations - A [Collection] of all the [Annotation]s defined on the currently running test method + */ +fun getScreenshotInstrumentationAnnotation( + classAnnotations: List, + methodAnnotations: Collection? +): Annotation? { + val annotationName = getScreenshotAnnotationName() + return classAnnotations.findAnnotation(annotationName) ?: methodAnnotations?.findAnnotation(annotationName) } diff --git a/Library/src/main/java/dev/testify/internal/TestifyConfiguration.kt b/Library/src/main/java/dev/testify/internal/TestifyConfiguration.kt index 5e94f233..b405403c 100644 --- a/Library/src/main/java/dev/testify/internal/TestifyConfiguration.kt +++ b/Library/src/main/java/dev/testify/internal/TestifyConfiguration.kt @@ -38,7 +38,7 @@ import dev.testify.CaptureMethod import dev.testify.CompareMethod import dev.testify.annotation.BitmapComparisonExactness import dev.testify.annotation.IgnoreScreenshot -import dev.testify.annotation.getAnnotation +import dev.testify.annotation.findAnnotation import dev.testify.internal.exception.ScreenshotTestIgnoredException import dev.testify.internal.helpers.OrientationHelper import dev.testify.internal.helpers.ResourceWrapper @@ -106,12 +106,12 @@ data class TestifyConfiguration( * Update the internal configuration values based on any annotations that may be present on the test method */ internal fun applyAnnotations(methodAnnotations: Collection?) { - val bitmapComparison = methodAnnotations?.getAnnotation() + val bitmapComparison = methodAnnotations?.findAnnotation() if (exactness == null) { exactness = bitmapComparison?.exactness } - ignoreAnnotation = methodAnnotations?.getAnnotation() + ignoreAnnotation = methodAnnotations?.findAnnotation() } private fun IgnoreScreenshot?.isIgnored(activity: Activity): Boolean { diff --git a/Library/src/main/java/dev/testify/internal/extensions/InstrumentationRegistryExtensions.kt b/Library/src/main/java/dev/testify/internal/extensions/InstrumentationRegistryExtensions.kt index 88770c47..5378cd7f 100644 --- a/Library/src/main/java/dev/testify/internal/extensions/InstrumentationRegistryExtensions.kt +++ b/Library/src/main/java/dev/testify/internal/extensions/InstrumentationRegistryExtensions.kt @@ -26,7 +26,6 @@ package dev.testify.internal.extensions import android.app.Instrumentation import android.os.Bundle import androidx.test.platform.app.InstrumentationRegistry -import dev.testify.annotation.ScreenshotInstrumentation import dev.testify.internal.helpers.ManifestPlaceholder import dev.testify.internal.helpers.getMetaDataValue @@ -81,12 +80,6 @@ class TestInstrumentationRegistry { fun isInvokedFromPlugin(): Boolean = InstrumentationRegistry.getArguments().containsKey("annotation") -/** - * Returns the fully qualified dot-separated name of the annotation required by the Gradle plugin. - */ -fun getScreenshotAnnotationName(): String = - InstrumentationRegistry.getArguments().getString("annotation", ScreenshotInstrumentation::class.qualifiedName) - private const val ESC_YELLOW = "${27.toChar()}[33m" private const val ESC_CYAN = "${27.toChar()}[36m" private const val ESC_RESET = "${27.toChar()}[0m" From cc0fbda8a182716cef6ffe3a92ab23b41e16b918 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 16:16:29 -0400 Subject: [PATCH 09/13] 41: Add interfaces for AssertionState and ScreenshotLifecycleHost --- CHANGELOG.md | 2 + .../main/java/dev/testify/ScreenshotRule.kt | 45 ++++++------ .../testify/internal/logic/AssertionState.kt | 70 +++++++++++++++++++ .../testify/internal/logic/InitiliazeView.kt | 2 + .../internal/logic/ScreenshotLifecycleHost.kt | 68 ++++++++++++++++++ 5 files changed, 163 insertions(+), 24 deletions(-) create mode 100644 Library/src/main/java/dev/testify/internal/logic/AssertionState.kt create mode 100644 Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt create mode 100644 Library/src/main/java/dev/testify/internal/logic/ScreenshotLifecycleHost.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d0afbf..2cd904d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ - `isRunningOnUiThread()` added as a top-level function. - `outputFileName()` added as an extension method for `Context`. +- Interface `AssertionState` +- Interface `ScreenshotLifecycleHost` #### Removed diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index ebe9a63e..fcbfdfa5 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -74,6 +74,9 @@ import dev.testify.internal.helpers.findRootView import dev.testify.internal.helpers.isRunningOnUiThread import dev.testify.internal.helpers.outputFileName import dev.testify.internal.helpers.registerActivityProvider +import dev.testify.internal.logic.AssertionState +import dev.testify.internal.logic.ScreenshotLifecycleHost +import dev.testify.internal.logic.ScreenshotLifecycleObserver import dev.testify.internal.logic.compareBitmaps import dev.testify.internal.logic.takeScreenshot import dev.testify.internal.processor.capture.createBitmapFromDrawingCache @@ -91,11 +94,10 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isRecordMode as recordMode - @Suppress("unused", "MemberVisibilityCanBePrivate") open class ScreenshotRule @JvmOverloads constructor( protected val activityClass: Class, - @IdRes var rootViewId: Int = android.R.id.content, + @IdRes override var rootViewId: Int = android.R.id.content, initialTouchMode: Boolean = false, enableReporter: Boolean = false, protected val configuration: TestifyConfiguration = TestifyConfiguration() @@ -103,6 +105,8 @@ open class ScreenshotRule @JvmOverloads constructor( TestRule, ActivityProvider, ScreenshotLifecycle, + AssertionState, + ScreenshotLifecycleHost by ScreenshotLifecycleObserver(), CompatibilityMethods, T> by ScreenshotRuleCompatibilityMethods() { @Deprecated( @@ -123,14 +127,14 @@ open class ScreenshotRule @JvmOverloads constructor( configuration = TestifyConfiguration() ) - @LayoutRes private var targetLayoutId: Int = NO_ID + @LayoutRes override var targetLayoutId: Int = NO_ID internal val testContext = getInstrumentation().context - private var assertSameInvoked = false + override var assertSameInvoked = false internal val espressoHelper: EspressoHelper by lazy { EspressoHelper(configuration) } - private var screenshotViewProvider: ViewProvider? = null - private var throwable: Throwable? = null - private var viewModification: ViewModification? = null + override var screenshotViewProvider: ViewProvider? = null + override var throwable: Throwable? = null + override var viewModification: ViewModification? = null private var extrasProvider: ExtrasProvider? = null private var isRecordMode: Boolean = false @@ -138,13 +142,11 @@ open class ScreenshotRule @JvmOverloads constructor( internal var reporter: Reporter? = null private set private lateinit var outputFileName: String - private val screenshotLifecycleObservers = HashSet() init { if (enableReporter || TestifyFeatures.Reporter.isEnabled(getInstrumentation().context)) { reporter = Reporter.create(getInstrumentation().targetContext, ReportSession()) } - addScreenshotObserver(TestifyFeatures) } fun setRootViewId(@IdRes rootViewId: Int): ScreenshotRule { @@ -173,7 +175,7 @@ open class ScreenshotRule @JvmOverloads constructor( return this } - fun setScreenshotViewProvider(viewProvider: ViewProvider): ScreenshotRule { + override fun setScreenshotViewProvider(viewProvider: ViewProvider): ScreenshotRule { this.screenshotViewProvider = viewProvider return this } @@ -207,7 +209,7 @@ open class ScreenshotRule @JvmOverloads constructor( super.afterActivityLaunched() ResourceWrapper.afterActivityLaunched(activity) configuration.afterActivityLaunched(activity) - screenshotLifecycleObservers.forEach { it.applyConfiguration(activity, configuration) } + notifyObservers { it.applyConfiguration(activity, configuration) } } @CallSuper @@ -227,6 +229,8 @@ open class ScreenshotRule @JvmOverloads constructor( * @return a new statement, which may be the same as base, a wrapper around base, or a completely new [Statement]. */ override fun apply(base: Statement, description: Description): Statement { + addScreenshotObserver(TestifyFeatures) + withRule(this) val methodAnnotations = description.annotations @@ -348,14 +352,6 @@ open class ScreenshotRule @JvmOverloads constructor( } } - fun addScreenshotObserver(observer: ScreenshotLifecycle) { - this.screenshotLifecycleObservers.add(observer) - } - - fun removeScreenshotObserver(observer: ScreenshotLifecycle) { - this.screenshotLifecycleObservers.remove(observer) - } - /** * Test lifecycle method. * Invoked immediately before assertSame and before the activity is launched. @@ -386,7 +382,8 @@ open class ScreenshotRule @JvmOverloads constructor( fun assertSame() { assertSameInvoked = true addScreenshotObserver(this) - screenshotLifecycleObservers.forEach { it.beforeAssertSame() } + + notifyObservers { it.beforeAssertSame() } if (isRunningOnUiThread()) { throw NoScreenshotsOnUiThreadException() @@ -406,9 +403,9 @@ open class ScreenshotRule @JvmOverloads constructor( reporter?.captureOutput(this) outputFileName = testContext.outputFileName(description) - screenshotLifecycleObservers.forEach { it.beforeInitializeView(activity) } + notifyObservers { it.beforeInitializeView(activity) } initializeView(activity) - screenshotLifecycleObservers.forEach { it.afterInitializeView(activity) } + notifyObservers { it.afterInitializeView(activity) } espressoHelper.beforeScreenshot() @@ -417,7 +414,7 @@ open class ScreenshotRule @JvmOverloads constructor( configuration.beforeScreenshot(rootView) - screenshotLifecycleObservers.forEach { it.beforeScreenshot(activity) } + notifyObservers { it.beforeScreenshot(activity) } val currentBitmap = takeScreenshot( activity, @@ -426,7 +423,7 @@ open class ScreenshotRule @JvmOverloads constructor( configuration.captureMethod ?: ::createBitmapFromDrawingCache ) ?: throw FailedToCaptureBitmapException() - screenshotLifecycleObservers.forEach { it.afterScreenshot(activity, currentBitmap) } + notifyObservers { it.afterScreenshot(activity, currentBitmap) } if (configuration.pauseForInspection) { Thread.sleep(LAYOUT_INSPECTION_TIME_MS.toLong()) diff --git a/Library/src/main/java/dev/testify/internal/logic/AssertionState.kt b/Library/src/main/java/dev/testify/internal/logic/AssertionState.kt new file mode 100644 index 00000000..2c11054b --- /dev/null +++ b/Library/src/main/java/dev/testify/internal/logic/AssertionState.kt @@ -0,0 +1,70 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify.internal.logic + +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes +import dev.testify.ViewModification +import dev.testify.ViewProvider + +/** + * This interface defines required internal state for the assertSame() logic. + */ +interface AssertionState { + + /** + * Track if the assertSame() method was invoked by the caller. + */ + var assertSameInvoked: Boolean + + /** + * Caught exception that thrown during assertSame(). + */ + var throwable: Throwable? + + /** + * The ID of the [android.view.View] to screenshot. + */ + @get:IdRes var rootViewId: Int + + /** + * The ID of the XML layout file to be inflated. + */ + @get:LayoutRes var targetLayoutId: Int + + /** + * The [ViewProvider] instance used for taking a screenshot. + */ + val screenshotViewProvider: ViewProvider? + + /** + * The [ViewModification] to apply to the activity prior to taking the screenshot. + */ + var viewModification: ViewModification? + + /** + * Builder method for setting [screenshotViewProvider] + */ + fun setScreenshotViewProvider(viewProvider: ViewProvider): AssertionState +} diff --git a/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt b/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt new file mode 100644 index 00000000..661f9050 --- /dev/null +++ b/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt @@ -0,0 +1,2 @@ +package dev.testify.internal.logic + diff --git a/Library/src/main/java/dev/testify/internal/logic/ScreenshotLifecycleHost.kt b/Library/src/main/java/dev/testify/internal/logic/ScreenshotLifecycleHost.kt new file mode 100644 index 00000000..c0dafc27 --- /dev/null +++ b/Library/src/main/java/dev/testify/internal/logic/ScreenshotLifecycleHost.kt @@ -0,0 +1,68 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify.internal.logic + +import dev.testify.ScreenshotLifecycle + +/** + * Implementers of this interface provide all the expected callbacks to [ScreenshotLifecycle] + */ +interface ScreenshotLifecycleHost { + + /** + * Subscribe [observer] for all [ScreenshotLifecycle] event callbacks + * + * @return true if successfully subscribed + */ + fun addScreenshotObserver(observer: ScreenshotLifecycle): Boolean + + /** + * Unsubscribe [observer] for [ScreenshotLifecycle] event callbacks + * + * @return true if successfully unsubscribed + */ + fun removeScreenshotObserver(observer: ScreenshotLifecycle): Boolean + + /** + * Notify all subscribed observers of the [ScreenshotLifecycle] event + */ + fun notifyObservers(event: (ScreenshotLifecycle) -> Unit) +} + +/** + * Default implementation of [ScreenshotLifecycleHost] + */ +class ScreenshotLifecycleObserver : ScreenshotLifecycleHost { + + private val screenshotLifecycleObservers = HashSet() + + override fun addScreenshotObserver(observer: ScreenshotLifecycle) = + this.screenshotLifecycleObservers.add(observer) + + override fun removeScreenshotObserver(observer: ScreenshotLifecycle) = + this.screenshotLifecycleObservers.remove(observer) + + override fun notifyObservers(event: (ScreenshotLifecycle) -> Unit) = + screenshotLifecycleObservers.forEach(event) +} From 2162eb04ec630895d0d74319b712853b72193e88 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 16:59:37 -0400 Subject: [PATCH 10/13] 41: Extract initializeView() --- CHANGELOG.md | 2 + .../testify/ScreenshotRuleLifecycleTest.kt | 38 +++++---- .../main/java/dev/testify/ScreenshotRule.kt | 52 +----------- .../testify/internal/logic/InitiliazeView.kt | 85 +++++++++++++++++++ 4 files changed, 109 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd904d9..ee8774c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - `ScreenshotRule.getScreenshotInstrumentationAnnotation()` is now a top-level function. - `Collection.getAnnotation()` renamed to `Collection.findAnnotation()`. - Package for `getScreenshotAnnotationName()` changed from `dev.testify.internal.extensions` to `dev.testify.annotation`. +- `ScreenshotRule.initializeView()` is now a top-level function. #### Added @@ -25,6 +26,7 @@ #### Removed - `open fun ScreenshotRule.generateHighContrastDiff(baselineBitmap: Bitmap, currentBitmap: Bitmap)` has been removed. Use `class HighContrastDiff` directly. +- `ScreenshotRule.applyViewModifications()` has been removed. Use `TestifyConfiguration.applyViewModificationsMainThread()` instead. #### Fixed diff --git a/Library/src/androidTest/java/dev/testify/ScreenshotRuleLifecycleTest.kt b/Library/src/androidTest/java/dev/testify/ScreenshotRuleLifecycleTest.kt index 3bbc05ef..3f6bb0ad 100644 --- a/Library/src/androidTest/java/dev/testify/ScreenshotRuleLifecycleTest.kt +++ b/Library/src/androidTest/java/dev/testify/ScreenshotRuleLifecycleTest.kt @@ -26,8 +26,10 @@ package dev.testify import android.app.Activity import android.graphics.Bitmap -import android.view.ViewGroup import dev.testify.annotation.ScreenshotInstrumentation +import dev.testify.internal.TestifyConfiguration +import io.mockk.every +import io.mockk.spyk import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Rule @@ -35,10 +37,14 @@ import org.junit.Test class ScreenshotRuleLifecycleTest { - inner class TestScreenshotRule : ScreenshotRule(TestActivity::class.java) { - override fun applyViewModifications(parentView: ViewGroup) { - methodOrder.add("applyViewModifications") - } + private val configuration = spyk(TestifyConfiguration()) { + every { applyViewModificationsMainThread(any()) } answers { methodOrder.add("applyViewModifications") } + } + + inner class TestScreenshotRule : ScreenshotRule( + activityClass = TestActivity::class.java, + configuration = configuration + ) { override fun beforeActivityLaunched() { methodOrder.add("beforeActivityLaunched") @@ -79,18 +85,14 @@ class ScreenshotRuleLifecycleTest { @Test fun default() { rule.assertSame() - assertEquals( - listOf( - "beforeAssertSame", - "beforeActivityLaunched", - "afterActivityLaunched", - "beforeInitializeView", - "applyViewModifications", - "afterInitializeView", - "beforeScreenshot", - "afterScreenshot" - ), - methodOrder - ) + + assertEquals("beforeAssertSame", methodOrder[0]) + assertEquals("beforeActivityLaunched", methodOrder[1]) + assertEquals("afterActivityLaunched", methodOrder[2]) + assertEquals("beforeInitializeView", methodOrder[3]) + assertEquals("applyViewModifications", methodOrder[4]) + assertEquals("afterInitializeView", methodOrder[5]) + assertEquals("beforeScreenshot", methodOrder[6]) + assertEquals("afterScreenshot", methodOrder[7]) } } diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index fcbfdfa5..d8d7b12a 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -30,13 +30,10 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Bundle -import android.os.Debug import android.view.View -import android.view.ViewGroup import androidx.annotation.CallSuper import androidx.annotation.IdRes import androidx.annotation.LayoutRes -import androidx.annotation.UiThread import androidx.annotation.VisibleForTesting import androidx.test.annotation.ExperimentalTestApi import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation @@ -60,7 +57,6 @@ import dev.testify.internal.exception.NoScreenshotsOnUiThreadException import dev.testify.internal.exception.ScreenshotBaselineNotDefinedException import dev.testify.internal.exception.ScreenshotIsDifferentException import dev.testify.internal.exception.ScreenshotTestIgnoredException -import dev.testify.internal.exception.ViewModificationException import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.getModuleName import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.instrumentationPrintln import dev.testify.internal.extensions.cyan @@ -78,6 +74,7 @@ import dev.testify.internal.logic.AssertionState import dev.testify.internal.logic.ScreenshotLifecycleHost import dev.testify.internal.logic.ScreenshotLifecycleObserver import dev.testify.internal.logic.compareBitmaps +import dev.testify.internal.logic.initializeView import dev.testify.internal.logic.takeScreenshot import dev.testify.internal.processor.capture.createBitmapFromDrawingCache import dev.testify.internal.processor.diff.HighContrastDiff @@ -90,8 +87,6 @@ import org.junit.AssumptionViolatedException import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isRecordMode as recordMode @Suppress("unused", "MemberVisibilityCanBePrivate") @@ -404,7 +399,7 @@ open class ScreenshotRule @JvmOverloads constructor( outputFileName = testContext.outputFileName(description) notifyObservers { it.beforeInitializeView(activity) } - initializeView(activity) + initializeView(activityProvider = this, assertionState = this, configuration = configuration) notifyObservers { it.afterInitializeView(activity) } espressoHelper.beforeScreenshot() @@ -497,48 +492,6 @@ open class ScreenshotRule @JvmOverloads constructor( } } - @UiThread - @CallSuper - open fun applyViewModifications(parentView: ViewGroup) { - configuration.applyViewModificationsMainThread(parentView) - } - - @VisibleForTesting - internal fun initializeView(activity: Activity) { - val parentView = activity.findRootView(rootViewId) - val latch = CountDownLatch(1) - - var viewModificationException: Throwable? = null - activity.runOnUiThread { - if (targetLayoutId != NO_ID) { - activity.layoutInflater.inflate(targetLayoutId, parentView, true) - } - - viewModification?.let { viewModification -> - try { - viewModification(parentView) - } catch (exception: Throwable) { - viewModificationException = exception - } - } - - applyViewModifications(parentView) - - latch.countDown() - } - configuration.applyViewModificationsTestThread(activity) - - if (Debug.isDebuggerConnected()) { - latch.await() - } else { - assertTrue(latch.await(INFLATE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) - } - - viewModificationException?.let { - throw ViewModificationException(it) - } - } - private inner class ScreenshotStatement constructor(private val base: Statement) : Statement() { override fun evaluate() { @@ -592,6 +545,5 @@ open class ScreenshotRule @JvmOverloads constructor( companion object { const val NO_ID = -1 private const val LAYOUT_INSPECTION_TIME_MS = 60000 - private const val INFLATE_TIMEOUT_SECONDS: Long = 5 } } diff --git a/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt b/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt index 661f9050..4ddfd2c1 100644 --- a/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt +++ b/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt @@ -1,2 +1,87 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package dev.testify.internal.logic +import android.app.Activity +import android.os.Debug +import dev.testify.ScreenshotRule +import dev.testify.internal.TestifyConfiguration +import dev.testify.internal.exception.ViewModificationException +import dev.testify.internal.helpers.ActivityProvider +import dev.testify.internal.helpers.findRootView +import org.junit.Assert +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +private const val INFLATE_TIMEOUT_SECONDS: Long = 5 + +/** + * Prepares the [android.view.View] under test for the screenshot. + * + * Inflates the layout, if necessary. + * Applies the view modifications. + * + * @param activityProvider - Required to access the current [Activity] + * @param assertionState - [AssertionState] for the current test + * @param configuration - [TestifyConfiguration] used to configure the view + */ +fun initializeView( + activityProvider: ActivityProvider, + assertionState: AssertionState, + configuration: TestifyConfiguration +) { + val activity = activityProvider.getActivity() + val parentView = activity.findRootView(assertionState.rootViewId) + val latch = CountDownLatch(1) + + var viewModificationException: Throwable? = null + activity.runOnUiThread { + if (assertionState.targetLayoutId != ScreenshotRule.NO_ID) { + activity.layoutInflater.inflate(assertionState.targetLayoutId, parentView, true) + } + + assertionState.viewModification?.let { viewModification -> + try { + viewModification(parentView) + } catch (exception: Throwable) { + viewModificationException = exception + } + } + + configuration.applyViewModificationsMainThread(parentView) + + latch.countDown() + } + configuration.applyViewModificationsTestThread(activity) + + if (Debug.isDebuggerConnected()) { + latch.await() + } else { + Assert.assertTrue(latch.await(INFLATE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + } + + viewModificationException?.let { + throw ViewModificationException(it) + } +} From c07441618b534cdf8377b6d5cf7bb0c4307948b2 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 20:42:34 -0400 Subject: [PATCH 11/13] 41: Reporter no longer requires a ScreenshotRule argument --- .../main/java/dev/testify/report/Reporter.kt | 47 ++++++++----------- .../src/test/java/dev/testify/ReporterTest.kt | 15 +++--- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/Library/src/main/java/dev/testify/report/Reporter.kt b/Library/src/main/java/dev/testify/report/Reporter.kt index a292e4c0..a51c243f 100644 --- a/Library/src/main/java/dev/testify/report/Reporter.kt +++ b/Library/src/main/java/dev/testify/report/Reporter.kt @@ -85,9 +85,9 @@ internal open class Reporter protected constructor( * At this point in the execution, Testify can correctly identify the baseline path as all * modifications have been applied */ - fun captureOutput(rule: ScreenshotRule<*>) { - builder.appendLine("baseline_image: assets/${getBaselinePath(rule)}", indent = 8) - builder.appendLine("test_image: ${getOutputPath(rule)}", indent = 8) + fun captureOutput() { + builder.appendLine("baseline_image: assets/${getBaselinePath()}", indent = 8) + builder.appendLine("test_image: ${getOutputPath()}", indent = 8) } /** @@ -134,43 +134,36 @@ internal open class Reporter protected constructor( } @VisibleForTesting - open fun writeToFile(builder: StringBuilder, file: File) { + open fun writeToFile(builder: StringBuilder, file: File) = file.appendText(builder.toString()) - } - private fun StringBuilder.appendLine(value: String, indent: Int): StringBuilder { - return append("".padStart(indent)).appendLine(value) - } + private fun StringBuilder.appendLine(value: String, indent: Int): StringBuilder = + append("".padStart(indent)).appendLine(value) @VisibleForTesting - internal open fun getBaselinePath(rule: ScreenshotRule<*>): String { - return getFileRelativeToRoot( + internal open fun getBaselinePath(): String = + getFileRelativeToRoot( subpath = getDeviceDescription(context), fileName = testDescription.methodName, extension = PNG_EXTENSION ) - } - private val ScreenshotRule<*>.fileName: String - get() { - return formatDeviceString( - DeviceStringFormatter( - this.testContext, - testDescription.nameComponents - ), - DEFAULT_NAME_FORMAT - ) - } + private val Context.fileName: String + get() = formatDeviceString( + DeviceStringFormatter( + this, + testDescription.nameComponents + ), + DEFAULT_NAME_FORMAT + ) @VisibleForTesting - internal open fun getOutputPath(rule: ScreenshotRule<*>): String { - return getDestination(context, rule.fileName).description - } + internal open fun getOutputPath(): String = + getDestination(context, context.fileName).description @VisibleForTesting - internal open fun getEnvironmentArguments(): Bundle { - return InstrumentationRegistry.getArguments() - } + internal open fun getEnvironmentArguments(): Bundle = + InstrumentationRegistry.getArguments() private fun getDestination(): Destination = getDestination( context = context, diff --git a/Library/src/test/java/dev/testify/ReporterTest.kt b/Library/src/test/java/dev/testify/ReporterTest.kt index 45580636..74d2fec3 100644 --- a/Library/src/test/java/dev/testify/ReporterTest.kt +++ b/Library/src/test/java/dev/testify/ReporterTest.kt @@ -47,7 +47,6 @@ internal open class ReporterTest { private lateinit var mockContext: Context private lateinit var mockSession: ReportSession private val mockInstrumentation: Instrumentation = mockk() - private val mockRule: ScreenshotRule<*> = mockk() private val mockTestClass: Class<*> = ReporterTest::class.java private var mockDescription = TestDescription("startTest", mockTestClass) private val mockFile: File = mockk() @@ -84,8 +83,8 @@ internal open class ReporterTest { } private fun Reporter.configureMocks(body: List? = null) { - every { getBaselinePath(any()) } returns "foo" - every { getOutputPath(any()) } returns "bar" + every { getBaselinePath() } returns "foo" + every { getOutputPath() } returns "bar" every { getReportFile() } returns mockFile every { writeToFile(any(), any()) } just runs every { clearFile(mockFile) } just runs @@ -113,7 +112,7 @@ internal open class ReporterTest { @Test fun `captureOutput() produces the expected yaml`() { - reporter.captureOutput(mockRule) + reporter.captureOutput() assertEquals( " baseline_image: assets/foo\n" + " test_image: bar\n", @@ -206,7 +205,7 @@ internal open class ReporterTest { reporter.startTest(mockDescription) reporter.identifySession(mockInstrumentation) - reporter.captureOutput(mockRule) + reporter.captureOutput() reporter.pass() reporter.endTest() @@ -237,21 +236,21 @@ internal open class ReporterTest { var reporter = setUpForFirstTest(spyk(ReportSession())) reporter.startTest(mockDescription) reporter.identifySession(mockInstrumentation) - reporter.captureOutput(mockRule) + reporter.captureOutput() reporter.pass() reporter.endTest() reporter = setUpForSecondTest() reporter.startTest(mockDescription) reporter.identifySession(mockInstrumentation) - reporter.captureOutput(mockRule) + reporter.captureOutput() reporter.fail(Exception("This is a failure")) reporter.endTest() reporter = setUpForThirdTest() reporter.startTest(mockDescription) reporter.identifySession(mockInstrumentation) - reporter.captureOutput(mockRule) + reporter.captureOutput() reporter.skip() reporter.endTest() From 8aebf90e8c953a8405b959267ee74877ddda7d92 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Fri, 15 Sep 2023 21:05:07 -0400 Subject: [PATCH 12/13] 41: Extract assertSame() --- CHANGELOG.md | 2 + .../dev/testify/ComposableScreenshotRule.kt | 1 + .../main/java/dev/testify/ScreenshotRule.kt | 155 ++------------ .../dev/testify/annotation/TestifyLayout.kt | 2 +- .../internal/helpers/ActivityProvider.kt | 2 + .../internal/helpers/EspressoHelper.kt | 6 +- .../dev/testify/internal/logic/AssertSame.kt | 191 ++++++++++++++++++ .../testify/internal/logic/InitiliazeView.kt | 4 +- .../processor/diff/HighContrastDiff.kt | 7 + 9 files changed, 229 insertions(+), 141 deletions(-) create mode 100644 Library/src/main/java/dev/testify/internal/logic/AssertSame.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index ee8774c1..5831b1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - `Collection.getAnnotation()` renamed to `Collection.findAnnotation()`. - Package for `getScreenshotAnnotationName()` changed from `dev.testify.internal.extensions` to `dev.testify.annotation`. - `ScreenshotRule.initializeView()` is now a top-level function. +- `EspressoHelper` now extends `ScreenshotLifecycle` and `beforeScreenshot()` has been replaced with `afterInitializeView()` #### Added @@ -22,6 +23,7 @@ - `outputFileName()` added as an extension method for `Context`. - Interface `AssertionState` - Interface `ScreenshotLifecycleHost` +- `assertSame()` is now available as a top-level function, decoupled from `ScreenshotRule` #### Removed diff --git a/Ext/Compose/src/main/java/dev/testify/ComposableScreenshotRule.kt b/Ext/Compose/src/main/java/dev/testify/ComposableScreenshotRule.kt index db479892..248ffc67 100644 --- a/Ext/Compose/src/main/java/dev/testify/ComposableScreenshotRule.kt +++ b/Ext/Compose/src/main/java/dev/testify/ComposableScreenshotRule.kt @@ -132,6 +132,7 @@ open class ComposableScreenshotRule( */ override fun afterInitializeView(activity: Activity) { composeActions?.invoke(composeTestRule) + composeTestRule.waitForIdle() super.afterInitializeView(activity) } diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index d8d7b12a..8780dd0b 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -30,7 +30,7 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Bundle -import android.view.View +import android.view.View.NO_ID import androidx.annotation.CallSuper import androidx.annotation.IdRes import androidx.annotation.LayoutRes @@ -42,47 +42,30 @@ import dev.testify.annotation.TestifyLayout import dev.testify.annotation.findAnnotation import dev.testify.annotation.getScreenshotAnnotationName import dev.testify.annotation.getScreenshotInstrumentationAnnotation -import dev.testify.internal.DEFAULT_FOLDER_FORMAT -import dev.testify.internal.DeviceStringFormatter import dev.testify.internal.ScreenshotRuleCompatibilityMethods import dev.testify.internal.TestifyConfiguration -import dev.testify.internal.assertExpectedDevice import dev.testify.internal.exception.ActivityNotRegisteredException import dev.testify.internal.exception.AssertSameMustBeLastException -import dev.testify.internal.exception.FailedToCaptureBitmapException import dev.testify.internal.exception.FinalizeDestinationException import dev.testify.internal.exception.MissingAssertSameException import dev.testify.internal.exception.MissingScreenshotInstrumentationAnnotationException -import dev.testify.internal.exception.NoScreenshotsOnUiThreadException -import dev.testify.internal.exception.ScreenshotBaselineNotDefinedException -import dev.testify.internal.exception.ScreenshotIsDifferentException import dev.testify.internal.exception.ScreenshotTestIgnoredException +import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isRecordMode import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.getModuleName import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.instrumentationPrintln import dev.testify.internal.extensions.cyan import dev.testify.internal.extensions.isInvokedFromPlugin -import dev.testify.internal.formatDeviceString import dev.testify.internal.helpers.ActivityProvider import dev.testify.internal.helpers.EspressoActions import dev.testify.internal.helpers.EspressoHelper import dev.testify.internal.helpers.ResourceWrapper -import dev.testify.internal.helpers.findRootView -import dev.testify.internal.helpers.isRunningOnUiThread -import dev.testify.internal.helpers.outputFileName import dev.testify.internal.helpers.registerActivityProvider import dev.testify.internal.logic.AssertionState import dev.testify.internal.logic.ScreenshotLifecycleHost import dev.testify.internal.logic.ScreenshotLifecycleObserver -import dev.testify.internal.logic.compareBitmaps -import dev.testify.internal.logic.initializeView -import dev.testify.internal.logic.takeScreenshot -import dev.testify.internal.processor.capture.createBitmapFromDrawingCache -import dev.testify.internal.processor.diff.HighContrastDiff -import dev.testify.output.getDestination +import dev.testify.internal.logic.assertSame import dev.testify.report.ReportSession import dev.testify.report.Reporter -import org.junit.Assert.assertTrue -import org.junit.Assume.assumeTrue import org.junit.AssumptionViolatedException import org.junit.rules.TestRule import org.junit.runner.Description @@ -257,6 +240,7 @@ open class ScreenshotRule @JvmOverloads constructor( configuration.applyAnnotations(methodAnnotations) espressoHelper.reset() + addScreenshotObserver(espressoHelper) getInstrumentation().testDescription = TestDescription( methodName = methodName, @@ -265,7 +249,7 @@ open class ScreenshotRule @JvmOverloads constructor( reporter?.startTest(getInstrumentation().testDescription) val testifyLayout = methodAnnotations?.findAnnotation() - targetLayoutId = testifyLayout?.resolvedLayoutId ?: View.NO_ID + targetLayoutId = testifyLayout?.resolvedLayoutId ?: NO_ID } @get:LayoutRes @@ -336,6 +320,10 @@ open class ScreenshotRule @JvmOverloads constructor( return intent } + override fun assureActivity(intent: Intent?) { + launchActivity(intent) + } + override fun launchActivity(startIntent: Intent?): T { try { return super.launchActivity(startIntent) @@ -375,120 +363,20 @@ open class ScreenshotRule @JvmOverloads constructor( @ExperimentalTestApi fun assertSame() { - assertSameInvoked = true addScreenshotObserver(this) - - notifyObservers { it.beforeAssertSame() } - - if (isRunningOnUiThread()) { - throw NoScreenshotsOnUiThreadException() - } - - try { - launchActivity(activityIntent) - } catch (e: ScreenshotTestIgnoredException) { - // Exit gracefully; mark test as ignored - assumeTrue(false) - return - } - try { - try { - val description = getInstrumentation().testDescription - reporter?.captureOutput(this) - outputFileName = testContext.outputFileName(description) - - notifyObservers { it.beforeInitializeView(activity) } - initializeView(activityProvider = this, assertionState = this, configuration = configuration) - notifyObservers { it.afterInitializeView(activity) } - - espressoHelper.beforeScreenshot() - - val rootView = activity.findRootView(rootViewId) - val screenshotView: View? = screenshotViewProvider?.invoke(rootView) - - configuration.beforeScreenshot(rootView) - - notifyObservers { it.beforeScreenshot(activity) } - - val currentBitmap = takeScreenshot( - activity, - outputFileName, - screenshotView, - configuration.captureMethod ?: ::createBitmapFromDrawingCache - ) ?: throw FailedToCaptureBitmapException() - - notifyObservers { it.afterScreenshot(activity, currentBitmap) } - - if (configuration.pauseForInspection) { - Thread.sleep(LAYOUT_INSPECTION_TIME_MS.toLong()) - } - - assertExpectedDevice(testContext, description.name, isRecordMode) - - val destination = getDestination(activity, outputFileName) - - val baselineBitmap = loadBaselineBitmapForComparison(testContext, description.name) - ?: if (isRecordMode || recordMode) { - instrumentationPrintln( - "\n\t✓ " + "Recording baseline for ${description.name}".cyan() - ) - - if (!destination.finalize()) - throw FinalizeDestinationException(destination.description) - - return - } else { - throw ScreenshotBaselineNotDefinedException( - moduleName = getModuleName(), - testName = description.name, - testClass = description.fullyQualifiedTestName, - deviceKey = formatDeviceString( - DeviceStringFormatter( - testContext, - null - ), - DEFAULT_FOLDER_FORMAT - ) - ) - } - - if (compareBitmaps(baselineBitmap, currentBitmap, configuration.getBitmapCompare())) { - assertTrue( - "Could not delete cached bitmap ${description.name}", - deleteBitmap(destination) - ) - } else { - if (!destination.finalize()) - throw FinalizeDestinationException(destination.description) - - if (TestifyFeatures.GenerateDiffs.isEnabled(activity)) { - HighContrastDiff(configuration.exclusionRects) - .name(outputFileName) - .baseline(baselineBitmap) - .current(currentBitmap) - .exactness(configuration.exactness) - .generate(context = activity) - } - if (isRecordMode || recordMode) { - instrumentationPrintln( - "\n\t✓ " + "Recording baseline for ${description.name}".cyan() - ) - } else { - throw ScreenshotIsDifferentException(getModuleName(), description.fullyQualifiedTestName) - } - } - } finally { - } + assertSame( + state = this, + configuration = configuration, + testContext = testContext, + screenshotLifecycleHost = this, + activityProvider = this, + activityIntent = activityIntent, + reporter = reporter + ) } finally { - ResourceWrapper.afterTestFinished(activity) - configuration.afterTestFinished() - TestifyFeatures.reset() removeScreenshotObserver(this) - if (throwable != null) { - //noinspection ThrowFromfinallyBlock - throw RuntimeException(throwable) - } + removeScreenshotObserver(espressoHelper) } } @@ -541,9 +429,4 @@ open class ScreenshotRule @JvmOverloads constructor( field = value assertSameInvoked = value } - - companion object { - const val NO_ID = -1 - private const val LAYOUT_INSPECTION_TIME_MS = 60000 - } } diff --git a/Library/src/main/java/dev/testify/annotation/TestifyLayout.kt b/Library/src/main/java/dev/testify/annotation/TestifyLayout.kt index 71491aba..c77426db 100644 --- a/Library/src/main/java/dev/testify/annotation/TestifyLayout.kt +++ b/Library/src/main/java/dev/testify/annotation/TestifyLayout.kt @@ -25,8 +25,8 @@ package dev.testify.annotation +import android.view.View.NO_ID import androidx.annotation.LayoutRes -import dev.testify.ScreenshotRule.Companion.NO_ID /** * The [TestifyLayout] annotation allows you to specify a layout resource to be automatically diff --git a/Library/src/main/java/dev/testify/internal/helpers/ActivityProvider.kt b/Library/src/main/java/dev/testify/internal/helpers/ActivityProvider.kt index 7a87dd7d..b7a8c239 100644 --- a/Library/src/main/java/dev/testify/internal/helpers/ActivityProvider.kt +++ b/Library/src/main/java/dev/testify/internal/helpers/ActivityProvider.kt @@ -26,9 +26,11 @@ package dev.testify.internal.helpers import android.app.Activity import android.app.Instrumentation +import android.content.Intent interface ActivityProvider { fun getActivity(): T + fun assureActivity(intent: Intent?) } private object ActivityProviderRegistry { diff --git a/Library/src/main/java/dev/testify/internal/helpers/EspressoHelper.kt b/Library/src/main/java/dev/testify/internal/helpers/EspressoHelper.kt index 58d3e437..1ac8c7ca 100644 --- a/Library/src/main/java/dev/testify/internal/helpers/EspressoHelper.kt +++ b/Library/src/main/java/dev/testify/internal/helpers/EspressoHelper.kt @@ -23,12 +23,14 @@ */ package dev.testify.internal.helpers +import android.app.Activity import androidx.test.espresso.Espresso +import dev.testify.ScreenshotLifecycle import dev.testify.internal.TestifyConfiguration typealias EspressoActions = () -> Unit -class EspressoHelper(private val configuration: TestifyConfiguration) { +class EspressoHelper(private val configuration: TestifyConfiguration) : ScreenshotLifecycle { var actions: EspressoActions? = null @@ -36,7 +38,7 @@ class EspressoHelper(private val configuration: TestifyConfiguration) { actions = null } - fun beforeScreenshot() { + override fun afterInitializeView(activity: Activity) { actions?.invoke() Espresso.onIdle() diff --git a/Library/src/main/java/dev/testify/internal/logic/AssertSame.kt b/Library/src/main/java/dev/testify/internal/logic/AssertSame.kt new file mode 100644 index 00000000..ff0e8dc3 --- /dev/null +++ b/Library/src/main/java/dev/testify/internal/logic/AssertSame.kt @@ -0,0 +1,191 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 ndtp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package dev.testify.internal.logic + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.view.View +import androidx.test.platform.app.InstrumentationRegistry +import dev.testify.TestifyFeatures +import dev.testify.deleteBitmap +import dev.testify.internal.DEFAULT_FOLDER_FORMAT +import dev.testify.internal.DeviceStringFormatter +import dev.testify.internal.TestifyConfiguration +import dev.testify.internal.assertExpectedDevice +import dev.testify.internal.exception.FailedToCaptureBitmapException +import dev.testify.internal.exception.NoScreenshotsOnUiThreadException +import dev.testify.internal.exception.ScreenshotBaselineNotDefinedException +import dev.testify.internal.exception.ScreenshotIsDifferentException +import dev.testify.internal.exception.ScreenshotTestIgnoredException +import dev.testify.internal.exception.TestifyException +import dev.testify.internal.extensions.TestInstrumentationRegistry +import dev.testify.internal.extensions.cyan +import dev.testify.internal.formatDeviceString +import dev.testify.internal.helpers.ActivityProvider +import dev.testify.internal.helpers.ResourceWrapper +import dev.testify.internal.helpers.findRootView +import dev.testify.internal.helpers.isRunningOnUiThread +import dev.testify.internal.helpers.outputFileName +import dev.testify.internal.processor.capture.createBitmapFromDrawingCache +import dev.testify.internal.processor.diff.HighContrastDiff +import dev.testify.loadBaselineBitmapForComparison +import dev.testify.output.getDestination +import dev.testify.report.Reporter +import dev.testify.testDescription +import org.junit.Assert +import org.junit.Assume + +/** + * Assert if the Activity matches the baseline screenshot. + * + * Using the provided [AssertionState] and [TestifyConfiguration], capture a bitmap of the Activity provided by + * [ActivityProvider] and compare it to the baseline image already recorded. + * + * @param state - the current state of the test + * @param configuration - a fully configured TestifyConfiguration instance + * @param testContext - the [Context] of the test runner + * @param screenshotLifecycleHost - an instance of [ScreenshotLifecycleHost] to notify of screenshot events + * @param activityProvider - an [ActivityProvider] which can provide a valid Activity instance + * @param activityIntent - optional, an [Intent] to pass to the [ActivityProvider] + * @param reporter - optional, an instance of [Reporter] which can log the test status + * + * @throws TestifyException + */ +internal fun assertSame( + state: AssertionState, + configuration: TestifyConfiguration, + testContext: Context, + screenshotLifecycleHost: ScreenshotLifecycleHost, + activityProvider: ActivityProvider, + activityIntent: Intent?, + reporter: Reporter? +) { + state.assertSameInvoked = true + + screenshotLifecycleHost.notifyObservers { it.beforeAssertSame() } + + if (isRunningOnUiThread()) { + throw NoScreenshotsOnUiThreadException() + } + + try { + activityProvider.assureActivity(activityIntent) + } catch (e: ScreenshotTestIgnoredException) { + // Exit gracefully; mark test as ignored + Assume.assumeTrue(false) + return + } + + var activity: TActivity? = null + + try { + activity = activityProvider.getActivity() + val description = InstrumentationRegistry.getInstrumentation().testDescription + reporter?.captureOutput() + val outputFileName = testContext.outputFileName(description) + + screenshotLifecycleHost.notifyObservers { it.beforeInitializeView(activity) } + initializeView(activityProvider = activityProvider, assertionState = state, configuration = configuration) + screenshotLifecycleHost.notifyObservers { it.afterInitializeView(activity) } + + val rootView = activity.findRootView(state.rootViewId) + val screenshotView: View? = state.screenshotViewProvider?.invoke(rootView) + + configuration.beforeScreenshot(rootView) + screenshotLifecycleHost.notifyObservers { it.beforeScreenshot(activity) } + + val currentBitmap = takeScreenshot( + activity, + outputFileName, + screenshotView, + configuration.captureMethod ?: ::createBitmapFromDrawingCache + ) ?: throw FailedToCaptureBitmapException() + + screenshotLifecycleHost.notifyObservers { it.afterScreenshot(activity, currentBitmap) } + + if (configuration.pauseForInspection) { + Thread.sleep(LAYOUT_INSPECTION_TIME_MS) + } + + assertExpectedDevice(testContext, description.name) + + val baselineBitmap = loadBaselineBitmapForComparison(testContext, description.name) + ?: if (TestInstrumentationRegistry.isRecordMode) { + TestInstrumentationRegistry.instrumentationPrintln( + "\n\t✓ " + "Recording baseline for ${description.name}".cyan() + ) + return + } else { + throw ScreenshotBaselineNotDefinedException( + moduleName = TestInstrumentationRegistry.getModuleName(), + testName = description.name, + testClass = description.fullyQualifiedTestName, + deviceKey = formatDeviceString( + DeviceStringFormatter( + testContext, + null + ), + DEFAULT_FOLDER_FORMAT + ) + ) + } + + if (compareBitmaps(baselineBitmap, currentBitmap, configuration.getBitmapCompare())) { + Assert.assertTrue( + "Could not delete cached bitmap ${description.name}", + deleteBitmap(getDestination(activity, outputFileName)) + ) + } else { + if (TestifyFeatures.GenerateDiffs.isEnabled(activity)) { + HighContrastDiff(configuration.exclusionRects) + .name(outputFileName) + .baseline(baselineBitmap) + .current(currentBitmap) + .exactness(configuration.exactness) + .generate(context = activity) + } + if (TestInstrumentationRegistry.isRecordMode) { + TestInstrumentationRegistry.instrumentationPrintln( + "\n\t✓ " + "Recording baseline for ${description.name}".cyan() + ) + } else { + throw ScreenshotIsDifferentException( + TestInstrumentationRegistry.getModuleName(), + description.fullyQualifiedTestName + ) + } + } + } finally { + activity?.let { ResourceWrapper.afterTestFinished(activity) } + configuration.afterTestFinished() + TestifyFeatures.reset() + if (state.throwable != null) { + //noinspection ThrowFromfinallyBlock + throw RuntimeException(state.throwable) + } + } +} + +private const val LAYOUT_INSPECTION_TIME_MS = 60000L diff --git a/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt b/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt index 4ddfd2c1..63fb8604 100644 --- a/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt +++ b/Library/src/main/java/dev/testify/internal/logic/InitiliazeView.kt @@ -25,7 +25,7 @@ package dev.testify.internal.logic import android.app.Activity import android.os.Debug -import dev.testify.ScreenshotRule +import android.view.View.NO_ID import dev.testify.internal.TestifyConfiguration import dev.testify.internal.exception.ViewModificationException import dev.testify.internal.helpers.ActivityProvider @@ -57,7 +57,7 @@ fun initializeView( var viewModificationException: Throwable? = null activity.runOnUiThread { - if (assertionState.targetLayoutId != ScreenshotRule.NO_ID) { + if (assertionState.targetLayoutId != NO_ID) { activity.layoutInflater.inflate(assertionState.targetLayoutId, parentView, true) } diff --git a/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt b/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt index 52d0a110..d2e3a493 100644 --- a/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt +++ b/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt @@ -40,6 +40,13 @@ import dev.testify.saveBitmapToDestination * * This diff image is a high-contrast image where each difference, regardless of how minor, is indicated in red * against a black background. + * + * Legend: + * - Black: Identical + * - Gray: Excluded from diff + * - Yellow: Different, but within allowable tolerances + * - Red: Different in excess of allowable tolerances + * */ class HighContrastDiff(private val exclusionRects: Set) { From 7a5671c52de8af9692959562221dcd28dbcb62f2 Mon Sep 17 00:00:00 2001 From: Daniel Jette Date: Mon, 9 Oct 2023 19:27:02 -0400 Subject: [PATCH 13/13] 41: Manually fix merge/rebase errors --- CHANGELOG.md | 5 --- .../dev/testify/ComposableScreenshotRule.kt | 9 +++++ .../dev/testify/AssertExpectedDeviceTest.kt | 5 ++- .../java/dev/testify/CompatibilityMethods.kt | 6 +++ .../main/java/dev/testify/ScreenshotRule.kt | 37 +------------------ .../java/dev/testify/ScreenshotUtility.kt | 1 - .../ScreenshotRuleCompatibilityMethods.kt | 11 ++++++ .../testify/internal/TestifyConfiguration.kt | 6 ++- .../dev/testify/internal/logic/AssertSame.kt | 19 ++++++++-- .../processor/diff/HighContrastDiff.kt | 2 - .../java/dev/testify/ScreenshotRuleTest.kt | 4 +- .../dev/testify/output/DestinationTest.kt | 11 ++++-- 12 files changed, 63 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5831b1a8..ed11f7fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,6 @@ #### Changed -- `ScreenshotRule.getRootView()` is now an extension function `fun Activity.findRootView(@IdRes rootViewId: Int): ViewGroup`. -- `ScreenshotRule.setCaptureMethod()` is deprecated. Use `var captureMethod: CaptureMethod?` on `TestifyConfiguration` to set the capture method. -- `ScreenshotRule.setCompareMethod()` is deprecated. Use `var compareMethod: CompareMethod?` on `TestifyConfiguration` to set the compare method. -- `ScreenshotRule.compareBitmaps()` is now a top-level function. -- `ScreenshotRule.takeScreenshot()` is now a top-level function. - `ScreenshotRule.getScreenshotInstrumentationAnnotation()` is now a top-level function. - `Collection.getAnnotation()` renamed to `Collection.findAnnotation()`. - Package for `getScreenshotAnnotationName()` changed from `dev.testify.internal.extensions` to `dev.testify.annotation`. diff --git a/Ext/Compose/src/main/java/dev/testify/ComposableScreenshotRule.kt b/Ext/Compose/src/main/java/dev/testify/ComposableScreenshotRule.kt index 248ffc67..a873262e 100644 --- a/Ext/Compose/src/main/java/dev/testify/ComposableScreenshotRule.kt +++ b/Ext/Compose/src/main/java/dev/testify/ComposableScreenshotRule.kt @@ -63,6 +63,15 @@ open class ComposableScreenshotRule( activity.disposeComposition() } + @Deprecated( + message = "Please use configure()", + replaceWith = ReplaceWith("configure { this@configure.captureMethod = captureMethod }") + ) + override fun setCaptureMethod(captureMethod: CaptureMethod?): ComposableScreenshotRule { + this.captureMethod = configuration.captureMethod ?: ::pixelCopyCapture + return this + } + /** * Set a screenshot view provider to capture only the @Composable bounds */ diff --git a/Library/src/androidTest/java/dev/testify/AssertExpectedDeviceTest.kt b/Library/src/androidTest/java/dev/testify/AssertExpectedDeviceTest.kt index efd1a186..4f422cdd 100644 --- a/Library/src/androidTest/java/dev/testify/AssertExpectedDeviceTest.kt +++ b/Library/src/androidTest/java/dev/testify/AssertExpectedDeviceTest.kt @@ -68,7 +68,10 @@ class AssertExpectedDeviceTest { @ScreenshotInstrumentation @Test fun testMissingBaselineRecordMode() { - rule.setRecordModeEnabled(true) + rule + .configure { + isRecordMode = true + } .assertSame() } diff --git a/Library/src/main/java/dev/testify/CompatibilityMethods.kt b/Library/src/main/java/dev/testify/CompatibilityMethods.kt index 48292d8f..3178e9f6 100644 --- a/Library/src/main/java/dev/testify/CompatibilityMethods.kt +++ b/Library/src/main/java/dev/testify/CompatibilityMethods.kt @@ -124,4 +124,10 @@ interface CompatibilityMethods, TActivity : Ac replaceWith = ReplaceWith("configure { this@configure.compareMethod = compareMethod }") ) fun setCompareMethod(compareMethod: CompareMethod?): TRule + + @Deprecated( + message = "Please use configure()", + replaceWith = ReplaceWith("configure { this@configure.isRecordMode = isRecordMode }") + ) + fun setRecordModeEnabled(isRecordMode: Boolean): TRule } diff --git a/Library/src/main/java/dev/testify/ScreenshotRule.kt b/Library/src/main/java/dev/testify/ScreenshotRule.kt index 8780dd0b..b6f6e709 100644 --- a/Library/src/main/java/dev/testify/ScreenshotRule.kt +++ b/Library/src/main/java/dev/testify/ScreenshotRule.kt @@ -35,7 +35,6 @@ import androidx.annotation.CallSuper import androidx.annotation.IdRes import androidx.annotation.LayoutRes import androidx.annotation.VisibleForTesting -import androidx.test.annotation.ExperimentalTestApi import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.ActivityTestRule import dev.testify.annotation.TestifyLayout @@ -46,14 +45,9 @@ import dev.testify.internal.ScreenshotRuleCompatibilityMethods import dev.testify.internal.TestifyConfiguration import dev.testify.internal.exception.ActivityNotRegisteredException import dev.testify.internal.exception.AssertSameMustBeLastException -import dev.testify.internal.exception.FinalizeDestinationException import dev.testify.internal.exception.MissingAssertSameException import dev.testify.internal.exception.MissingScreenshotInstrumentationAnnotationException import dev.testify.internal.exception.ScreenshotTestIgnoredException -import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isRecordMode -import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.getModuleName -import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.instrumentationPrintln -import dev.testify.internal.extensions.cyan import dev.testify.internal.extensions.isInvokedFromPlugin import dev.testify.internal.helpers.ActivityProvider import dev.testify.internal.helpers.EspressoActions @@ -70,7 +64,6 @@ import org.junit.AssumptionViolatedException import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isRecordMode as recordMode @Suppress("unused", "MemberVisibilityCanBePrivate") open class ScreenshotRule @JvmOverloads constructor( @@ -114,7 +107,6 @@ open class ScreenshotRule @JvmOverloads constructor( override var throwable: Throwable? = null override var viewModification: ViewModification? = null private var extrasProvider: ExtrasProvider? = null - private var isRecordMode: Boolean = false @VisibleForTesting internal var reporter: Reporter? = null @@ -163,14 +155,6 @@ open class ScreenshotRule @JvmOverloads constructor( return this } - /** - * Record a new baseline when running the test - */ - fun setRecordModeEnabled(isRecordMode: Boolean): ScreenshotRule { - this.isRecordMode = isRecordMode - return this - } - /** * Set the configuration for the ScreenshotRule * @@ -213,7 +197,8 @@ open class ScreenshotRule @JvmOverloads constructor( val methodAnnotations = description.annotations apply(description.methodName, description.testClass, methodAnnotations) - statement = ScreenshotStatement(base) + val statement = ScreenshotStatement(base) + this.statement = statement return super.apply(statement, description) } @@ -344,24 +329,6 @@ open class ScreenshotRule @JvmOverloads constructor( getInstrumentation().registerActivityProvider(this) } - /** - * Given [baselineBitmap] and [currentBitmap], use [HighContrastDiff] to write a companion .diff image for the - * current test. - * - * This diff image is a high-contrast image where each difference, regardless of how minor, is indicated in red - * against a black background. - */ - @ExperimentalTestApi - open fun generateHighContrastDiff(baselineBitmap: Bitmap, currentBitmap: Bitmap) { - HighContrastDiff(configuration.exclusionRects) - .name(outputFileName) - .baseline(baselineBitmap) - .current(currentBitmap) - .exactness(configuration.exactness) - .generate(context = activity) - } - - @ExperimentalTestApi fun assertSame() { addScreenshotObserver(this) try { diff --git a/Library/src/main/java/dev/testify/ScreenshotUtility.kt b/Library/src/main/java/dev/testify/ScreenshotUtility.kt index 163f3e58..8a26a472 100644 --- a/Library/src/main/java/dev/testify/ScreenshotUtility.kt +++ b/Library/src/main/java/dev/testify/ScreenshotUtility.kt @@ -49,7 +49,6 @@ val preferredBitmapOptions: BitmapFactory.Options return options } -@ExperimentalTestApi fun saveBitmapToDestination(context: Context, bitmap: Bitmap?, destination: Destination): Boolean { if (bitmap == null) { return false diff --git a/Library/src/main/java/dev/testify/internal/ScreenshotRuleCompatibilityMethods.kt b/Library/src/main/java/dev/testify/internal/ScreenshotRuleCompatibilityMethods.kt index acece104..90e0f79f 100644 --- a/Library/src/main/java/dev/testify/internal/ScreenshotRuleCompatibilityMethods.kt +++ b/Library/src/main/java/dev/testify/internal/ScreenshotRuleCompatibilityMethods.kt @@ -216,4 +216,15 @@ internal class ScreenshotRuleCompatibilityMethods true + else -> false } } diff --git a/Library/src/main/java/dev/testify/internal/logic/AssertSame.kt b/Library/src/main/java/dev/testify/internal/logic/AssertSame.kt index ff0e8dc3..fbb28960 100644 --- a/Library/src/main/java/dev/testify/internal/logic/AssertSame.kt +++ b/Library/src/main/java/dev/testify/internal/logic/AssertSame.kt @@ -35,6 +35,7 @@ import dev.testify.internal.DeviceStringFormatter import dev.testify.internal.TestifyConfiguration import dev.testify.internal.assertExpectedDevice import dev.testify.internal.exception.FailedToCaptureBitmapException +import dev.testify.internal.exception.FinalizeDestinationException import dev.testify.internal.exception.NoScreenshotsOnUiThreadException import dev.testify.internal.exception.ScreenshotBaselineNotDefinedException import dev.testify.internal.exception.ScreenshotIsDifferentException @@ -82,6 +83,9 @@ internal fun assertSame( activityIntent: Intent?, reporter: Reporter? ) { + fun isRecordMode(): Boolean = + TestInstrumentationRegistry.isRecordMode || configuration.isRecordMode + state.assertSameInvoked = true screenshotLifecycleHost.notifyObservers { it.beforeAssertSame() } @@ -129,13 +133,19 @@ internal fun assertSame( Thread.sleep(LAYOUT_INSPECTION_TIME_MS) } - assertExpectedDevice(testContext, description.name) + val isRecordMode = isRecordMode() + + assertExpectedDevice(testContext, description.name, isRecordMode) + + val destination = getDestination(activity, outputFileName) val baselineBitmap = loadBaselineBitmapForComparison(testContext, description.name) - ?: if (TestInstrumentationRegistry.isRecordMode) { + ?: if (isRecordMode()) { TestInstrumentationRegistry.instrumentationPrintln( "\n\t✓ " + "Recording baseline for ${description.name}".cyan() ) + if (!destination.finalize()) + throw FinalizeDestinationException(destination.description) return } else { throw ScreenshotBaselineNotDefinedException( @@ -155,9 +165,12 @@ internal fun assertSame( if (compareBitmaps(baselineBitmap, currentBitmap, configuration.getBitmapCompare())) { Assert.assertTrue( "Could not delete cached bitmap ${description.name}", - deleteBitmap(getDestination(activity, outputFileName)) + deleteBitmap(destination) ) } else { + if (!destination.finalize()) + throw FinalizeDestinationException(destination.description) + if (TestifyFeatures.GenerateDiffs.isEnabled(activity)) { HighContrastDiff(configuration.exclusionRects) .name(outputFileName) diff --git a/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt b/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt index d2e3a493..3ce8d6ec 100644 --- a/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt +++ b/Library/src/main/java/dev/testify/internal/processor/diff/HighContrastDiff.kt @@ -27,7 +27,6 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.Color import android.graphics.Rect -import androidx.test.annotation.ExperimentalTestApi import dev.testify.internal.processor.ParallelPixelProcessor import dev.testify.internal.processor.compare.colorspace.calculateDeltaE import dev.testify.internal.processor.createBitmap @@ -54,7 +53,6 @@ class HighContrastDiff(private val exclusionRects: Set) { private lateinit var baselineBitmap: Bitmap private lateinit var currentBitmap: Bitmap - @ExperimentalTestApi fun generate(context: Context) { val transformResult = ParallelPixelProcessor .create() diff --git a/Library/src/test/java/dev/testify/ScreenshotRuleTest.kt b/Library/src/test/java/dev/testify/ScreenshotRuleTest.kt index 56daf20b..4e5b01b1 100644 --- a/Library/src/test/java/dev/testify/ScreenshotRuleTest.kt +++ b/Library/src/test/java/dev/testify/ScreenshotRuleTest.kt @@ -283,7 +283,9 @@ class ScreenshotRuleTest { every { loadBaselineBitmapForComparison(any(), any()) } returns null subject - .setRecordModeEnabled(true) + .configure { + isRecordMode = true + } .assertSame() verifyReporter() diff --git a/Library/src/test/java/dev/testify/output/DestinationTest.kt b/Library/src/test/java/dev/testify/output/DestinationTest.kt index 122a053f..6b44b4ca 100644 --- a/Library/src/test/java/dev/testify/output/DestinationTest.kt +++ b/Library/src/test/java/dev/testify/output/DestinationTest.kt @@ -54,6 +54,9 @@ class DestinationTest { @RelaxedMockK lateinit var mockInstrumentation: Instrumentation + private fun String.normalizePath() = + this.replace("\\", "/").replace(this.substring(0, this.indexOf(":") + 1), "") + @Before fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) @@ -91,7 +94,7 @@ class DestinationTest { ) assertEquals( "/data/user/0/dev.testify.sample/app_images/root/screenshots/33-1080x2200@420dp-en_CA/fileName.ext", - destination.description + destination.description.normalizePath() ) } @@ -106,7 +109,7 @@ class DestinationTest { ) assertEquals( "/data/user/0/dev.testify.sample/app_images/root/custom_key/fileName.ext", - destination.description + destination.description.normalizePath() ) } @@ -123,7 +126,7 @@ class DestinationTest { ) assertEquals( "/storage/emulated/0/Android/data/dev.testify.sample/files/root/33-1080x2200@420dp-en_CA/fileName.ext", - destination.description + destination.description.normalizePath() ) } @@ -154,7 +157,7 @@ class DestinationTest { assertTrue("destination is $destination", destination is TestStorageDestination) assertEquals( "/data/user/0/dev.testify.sample/app_images/root/screenshots/33-1080x2200@420dp-en_CA/fileName.ext", - destination.description + destination.description.normalizePath() ) } }