From ea3791806f4c992d0c23bcebe2cedbb26484b2ce Mon Sep 17 00:00:00 2001 From: Pierre Nicolas <pierre.nicolas@savoirfairelinux.com> Date: Mon, 11 Nov 2024 11:07:48 -0500 Subject: [PATCH] test: add screenshot feature Change-Id: I694037c34df607e3add8d9383fbc6bf6ea4b0070 --- ci/Jenkinsfile | 2 + ci/download_screenshots.sh | 23 +++++ .../java/cx/ring/NativeScreenshot.kt | 99 +++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100755 ci/download_screenshots.sh create mode 100644 jami-android/app/src/androidTest/java/cx/ring/NativeScreenshot.kt diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile index dfcf6e4cd..574b391a8 100644 --- a/ci/Jenkinsfile +++ b/ci/Jenkinsfile @@ -121,6 +121,8 @@ pipeline { errorOccurred = true } + sh 'cd /jami-client-android/ci && ./download_screenshots.sh' + // Archive tests output save it as Jenkins artifact sh 'cd /jami-client-android/ci/spoon-output && zip -r ../ui-test-output.zip *' archiveArtifacts artifacts: 'ci/ui-test-output.zip', allowEmptyArchive: false diff --git a/ci/download_screenshots.sh b/ci/download_screenshots.sh new file mode 100755 index 000000000..508fd6332 --- /dev/null +++ b/ci/download_screenshots.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Define the source directory on the Android device (emulator) +screenshotDir="/sdcard/Download/screenshots" + +# Define the destination directory on the local system +localDir="/jami-client-android/ci/spoon-output/screenshots" + +# Create the local directory if it doesn't exist +mkdir -p "$localDir" + +# Download the content of the directory from the device to the local directory +adb pull "$screenshotDir" "$localDir" + +# Check if the download was successful +if [ $? -eq 0 ]; then + echo "The content of directory '$screenshotDir' has been downloaded to '$localDir'." +else + echo "Error downloading the directory." +fi + +# Set ownership of the local directory and its contents to jenkins +chown -R jenkins:jenkins "$localDir" diff --git a/jami-android/app/src/androidTest/java/cx/ring/NativeScreenshot.kt b/jami-android/app/src/androidTest/java/cx/ring/NativeScreenshot.kt new file mode 100644 index 000000000..4a90a0dd8 --- /dev/null +++ b/jami-android/app/src/androidTest/java/cx/ring/NativeScreenshot.kt @@ -0,0 +1,99 @@ +package cx.ring + +import android.graphics.Bitmap +import android.os.Build +import android.os.Environment +import androidx.test.runner.screenshot.BasicScreenCaptureProcessor +import androidx.test.runner.screenshot.Screenshot +import net.jami.utils.Log +import java.io.File +import java.io.IOException +import java.util.regex.Pattern + +object NativeScreenshot { + private var methodName: String? = null + private var className: String? = null + private val SCREENSHOT_NAME_VALIDATION: Pattern = Pattern.compile("[a-zA-Z0-9_-]+") + /** + * Captures screenshot using Androidx Screenshot library and stores in the filesystem. + * Special Cases: + * If the screenshotName contains spaces or does not pass validation, the corresponding + * screenshot is not visible on BrowserStack's Dashboard. + * If there is any runtime exception while capturing screenshot, the method throws + * Exception and the test might fail if the exception is not handled properly. + * @param screenshotName a screenshot identifier + * @return path to the screenshot file + */ + fun capture(screenshotName: String): String { + Log.w("NativeScreenshot", "Capturing screenshot: $screenshotName") + + val testClass = findTestClassTraceElement(Thread.currentThread().stackTrace) + className = testClass.className.replace("[^A-Za-z0-9._-]".toRegex(), "_") + methodName = testClass.methodName + val screenCaptureProcessor = EspressoScreenCaptureProcessor() + if (!SCREENSHOT_NAME_VALIDATION.matcher(screenshotName).matches()) { + throw IllegalArgumentException("ScreenshotName must match ${SCREENSHOT_NAME_VALIDATION.pattern()}.") + } else { + val capture = Screenshot.capture() + capture.format = Bitmap.CompressFormat.PNG + capture.name = screenshotName + try { + return screenCaptureProcessor.process(capture) + } catch (e: IOException) { + throw RuntimeException("Unable to capture screenshot.", e) + } + } + } + /** + * Extracts the currently executing test's trace element based on the test runner + * or any framework being used. + * @param trace stacktrace of the currently running test + * @return StackTrace Element corresponding to the current test being executed. + */ + private fun findTestClassTraceElement(trace: Array<StackTraceElement>): StackTraceElement { + for (i in trace.indices.reversed()) { + val element = trace[i] + if ("android.test.InstrumentationTestCase" == element.className && "runMethod" == element.methodName) { + return extractStackElement(trace, i) + } + if ("org.junit.runners.model.FrameworkMethod\$1" == element.className && "runReflectiveCall" == element.methodName) { + return extractStackElement(trace, i) + } + if ("cucumber.runtime.model.CucumberFeature" == element.className && "run" == element.methodName) { + return extractStackElement(trace, i) + } + } + throw IllegalArgumentException("Could not find test class!") + } + /** + * Based on the test runner or framework being used, extracts the exact traceElement. + * @param trace stacktrace of the currently running test + * @param i a reference index + * @return trace element based on the index passed + */ + private fun extractStackElement(trace: Array<StackTraceElement>, i: Int): StackTraceElement { + val testClassTraceIndex = if (Build.VERSION.SDK_INT >= 23) i - 2 else i - 3 + return trace[testClassTraceIndex] + } + private class EspressoScreenCaptureProcessor : BasicScreenCaptureProcessor() { + companion object { + private const val SCREENSHOT = "screenshots" + } + init { + val screenshotDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString(), SCREENSHOT) + val classDir = File(screenshotDir, className) + Log.w("NativeScreenshot", "Screenshot directory: $classDir") + mDefaultScreenshotPath = File(classDir, methodName) + } + /** + * Converts the filename to a standard path to be stored on device. + * Example: "post_addition" converts to "1648038895211_post_addition" + * which is later suffixed by the file extension i.e. png. + * @param filename a screenshot identifier + * @return custom filename format + */ + override fun getFilename(filename: String): String { + return "${System.currentTimeMillis()}_$filename" + } + } +} \ No newline at end of file -- GitLab