diff --git a/.github/workflows/pull-request-jobs.yml b/.github/workflows/pull-request-jobs.yml index 35a74f1..c226468 100644 --- a/.github/workflows/pull-request-jobs.yml +++ b/.github/workflows/pull-request-jobs.yml @@ -124,3 +124,10 @@ jobs: name: Emulator-Test-Results-${{ matrix.api-level }} path: ./**/build/reports/androidTests/**/*.html retention-days: 1 + - name: Upload Test Screenshots + uses: actions/upload-artifact@v2 + if: always() + with: + name: Emulator-Test-Results-${{ matrix.api-level }} + path: ./**/build/testscreenshots/* + retention-days: 1 \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d5da099..3ed80a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -116,3 +116,4 @@ dependencies { androidTestImplementation testFixtures(project(':core')) } +apply from: '../gradlescripts/pull-screenshots.gradle' \ No newline at end of file diff --git a/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt b/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt index a402297..f2666d7 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.test.junit4.StateRestorationTester import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R +import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceIdling import org.fnives.test.showcase.compose.screen.AppNavigation import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage @@ -21,8 +22,7 @@ import org.koin.test.KoinTest @RunWith(AndroidJUnit4::class) class AuthComposeInstrumentedTest : KoinTest { - @get:Rule - val composeTestRule = createComposeRule() + private val composeTestRule = createComposeRule() private val stateRestorationTester = StateRestorationTester(composeTestRule) private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule() @@ -35,6 +35,8 @@ class AuthComposeInstrumentedTest : KoinTest { @JvmField val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) .around(dispatcherTestRule) + .around(composeTestRule) + .around(ScreenshotRule("test-showcase-compose")) @Before fun setup() { diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt index 1517396..5e44713 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt @@ -3,7 +3,9 @@ package org.fnives.test.showcase.ui.home import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule import org.fnives.test.showcase.android.testutil.activity.safeClose +import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor import org.fnives.test.showcase.model.content.FavouriteContent import org.fnives.test.showcase.network.mockserver.ContentData @@ -38,6 +40,8 @@ class MainActivityInstrumentedTest : KoinTest { val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) .around(mainDispatcherTestRule) .around(AsyncDiffUtilInstantTestRule()) + .around(SafeCloseActivityRule { activityScenario }) + .around(ScreenshotRule("test-showcase")) @Before fun setup() { @@ -48,7 +52,6 @@ class MainActivityInstrumentedTest : KoinTest { @After fun tearDown() { - activityScenario.safeClose() Intents.release() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt index 7b31b8c..c59f43d 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt @@ -4,7 +4,8 @@ import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R -import org.fnives.test.showcase.android.testutil.activity.safeClose +import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule +import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule @@ -32,6 +33,8 @@ class AuthActivityInstrumentedTest : KoinTest { @JvmField val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) .around(mainDispatcherTestRule) + .around(SafeCloseActivityRule { activityScenario }) + .around(ScreenshotRule("test-showcase")) @Before fun setup() { @@ -41,7 +44,6 @@ class AuthActivityInstrumentedTest : KoinTest { @After fun tearDown() { - activityScenario.safeClose() Intents.release() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt index 94e34d8..cd2101d 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt @@ -4,7 +4,8 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.fnives.test.showcase.android.testutil.activity.safeClose +import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule +import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin @@ -32,6 +33,8 @@ class SplashActivityInstrumentedTest : KoinTest { @JvmField val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) .around(mainDispatcherTestRule) + .around(SafeCloseActivityRule { activityScenario }) + .around(ScreenshotRule("test-showcase")) @Before fun setup() { @@ -41,7 +44,6 @@ class SplashActivityInstrumentedTest : KoinTest { @After fun tearDown() { - activityScenario.safeClose() Intents.release() } diff --git a/gradlescripts/pull-screenshots.gradle b/gradlescripts/pull-screenshots.gradle new file mode 100644 index 0000000..bd97a86 --- /dev/null +++ b/gradlescripts/pull-screenshots.gradle @@ -0,0 +1,63 @@ +// Variables: +// ext.screenshotsPackageName => the package name to pull from +// ext.screenshotsDirectory => the directory on the device to pull from inside the Pictures folder +// ext.screenshotsSavePath => where to pull the images +// ext.adbPath => Give adb path, if the script cant find it by itself from localProperties + +def propertyOrNull = { key -> + if (extensions.extraProperties.has(key)) { + return extensions.extraProperties.get(key) + } else { + return null + } +} + +def findAdbFromLocal = { + def localProperties = new File(rootDir, "local.properties") + if (localProperties.exists()) { + Properties properties = new Properties() + localProperties.withInputStream { instr -> properties.load(instr) } + def sdkDir = properties.getProperty('sdk.dir') + return "$sdkDir/platform-tools/adb" + } else { + System.err.println("WARNING: SDK dir not found by local properties!") + return null + } +} + +task pullScreenshots(type: Exec) { + group = 'Test' + description = 'Pull screenshots' + + def packageName = propertyOrNull("screenshotsPackageName") ?: "$android.defaultConfig.applicationId" + def screenshotDirectory = propertyOrNull("screenshotsDirectory") ?: "test-screenshots" + def fullPath = "/sdcard/Android/data/$packageName/files/Pictures/$screenshotDirectory/" + def savePath = propertyOrNull("screenshotsSavePath") ?: "build/testscreenshots/" + def adb = propertyOrNull("adbPath") ?: findAdbFromLocal() + + commandLine "$adb", 'pull', "$fullPath", "$savePath/" +} + +task removeScreenshotsFromDevice(type: Exec) { + group = 'Test' + description = 'Delete screenshots From Device' + + def packageName = propertyOrNull("screenshotsPackageName") ?: "$android.defaultConfig.applicationId" + def screenshotDirectory = propertyOrNull("screenshotsDirectory") ?: "test-screenshots" + def fullPath = "/sdcard/Android/data/$packageName/files/Pictures/$screenshotDirectory/" + def adb = propertyOrNull("adbPath") ?: findAdbFromLocal() + + commandLine "$adb", 'shell', 'rm', '-r', "$fullPath" +} + +task removeLocalScreenshots(type: Delete) { + def savePath = propertyOrNull("screenshotsSavePath") ?: "build/testscreenshots/" + + delete files("$savePath") +} + +afterEvaluate { + connectedDebugAndroidTest.finalizedBy pullScreenshots + pullScreenshots.finalizedBy removeScreenshotsFromDevice + clean.dependsOn(removeLocalScreenshots) +} \ No newline at end of file diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/activity/SafeCloseActivityRule.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/activity/SafeCloseActivityRule.kt new file mode 100644 index 0000000..3fcf7a0 --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/activity/SafeCloseActivityRule.kt @@ -0,0 +1,16 @@ +package org.fnives.test.showcase.android.testutil.activity + +import androidx.test.core.app.ActivityScenario +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +/** + * Test Rule which closes the given [scenario] safely when the Test is finished. + */ +class SafeCloseActivityRule(val scenario: () -> ActivityScenario<*>) : TestWatcher() { + + override fun finished(description: Description) { + super.finished(description) + scenario().safeClose() + } +} diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/ScreenshotRule.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/ScreenshotRule.kt new file mode 100644 index 0000000..36a7756 --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/ScreenshotRule.kt @@ -0,0 +1,68 @@ +package org.fnives.test.showcase.android.testutil.screenshot + +import android.graphics.Bitmap +import androidx.test.runner.screenshot.ScreenCapture +import androidx.test.runner.screenshot.ScreenCaptureProcessor +import androidx.test.runner.screenshot.Screenshot +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import java.io.IOException + +class ScreenshotRule( + private val prefix: String = "", + private val takeBefore: Boolean = false, + private val takeOnSuccess: Boolean = false, + private val takeOnFailure: Boolean = true, + private val timestampSuffix: Boolean = true, + private val processor: ScreenCaptureProcessor = basicScreenCaptureProcessor(), +) : TestWatcher() { + + override fun starting(description: Description) { + super.starting(description) + if (takeBefore) { + takeScreenshot(baseName = description.beforeTestScreenshotName) + } + } + + override fun failed(e: Throwable?, description: Description) { + super.failed(e, description) + if (takeOnFailure) { + takeScreenshot(baseName = description.failTestScreenshotName) + } + } + + override fun succeeded(description: Description) { + super.succeeded(description) + if (takeOnSuccess) { + takeScreenshot(baseName = description.successTestScreenshotName) + } + } + + fun takeScreenshot(prefix: String = this.prefix, baseName: String) { + val fileName = if (timestampSuffix) { + "$prefix-$baseName-${System.currentTimeMillis()}" + } else { + "$prefix-$baseName" + } + takeScreenshot(filename = fileName) + } + + @Suppress("PrintStackTrace") + private fun takeScreenshot(filename: String) { + val capture: ScreenCapture = Screenshot.capture() + capture.name = filename + capture.format = Bitmap.CompressFormat.JPEG + try { + capture.process(setOf(processor)) + } catch (e: IOException) { + e.printStackTrace() + } + } + + companion object { + val Description.testScreenshotName get() = "${testClass.simpleName}-$methodName" + val Description.beforeTestScreenshotName get() = "$testScreenshotName-BEFORE" + val Description.successTestScreenshotName get() = "$testScreenshotName-SUCCESS" + val Description.failTestScreenshotName get() = "$testScreenshotName-FAIL" + } +} diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/basicScreenCaptureAccess.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/basicScreenCaptureAccess.kt new file mode 100644 index 0000000..8da2e6a --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/basicScreenCaptureAccess.kt @@ -0,0 +1,7 @@ +@file:Suppress("PackageDirectoryMismatch") + +package androidx.test.runner.screenshot + +import java.io.File + +fun basicScreenCaptureProcessor(file: File) = BasicScreenCaptureProcessor(file) diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/basicScreenCaptureProcessor.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/basicScreenCaptureProcessor.kt new file mode 100644 index 0000000..982902c --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/screenshot/basicScreenCaptureProcessor.kt @@ -0,0 +1,12 @@ +package org.fnives.test.showcase.android.testutil.screenshot + +import android.os.Environment +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.screenshot.basicScreenCaptureProcessor +import java.io.File + +fun basicScreenCaptureProcessor(subDir: String = "test-screenshots") = + basicScreenCaptureProcessor(File(getTestPicturesDir(), subDir)) + +fun getTestPicturesDir() = + InstrumentationRegistry.getInstrumentation().targetContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)