|
| 1 | +package com.sample.browserstack.samplecalculator; |
| 2 | + |
| 3 | +import android.graphics.Bitmap; |
| 4 | +import android.os.Build; |
| 5 | +import android.os.Environment; |
| 6 | + |
| 7 | +import androidx.test.runner.screenshot.BasicScreenCaptureProcessor; |
| 8 | +import androidx.test.runner.screenshot.ScreenCapture; |
| 9 | +import androidx.test.runner.screenshot.Screenshot; |
| 10 | + |
| 11 | +import java.io.File; |
| 12 | +import java.io.IOException; |
| 13 | +import java.util.regex.Pattern; |
| 14 | + |
| 15 | +public final class NativeScreenshot { |
| 16 | + |
| 17 | + private static String methodName; |
| 18 | + private static String className; |
| 19 | + private static final Pattern SCREENSHOT_NAME_VALIDATION = Pattern.compile("[a-zA-Z0-9_-]+"); |
| 20 | + |
| 21 | + private NativeScreenshot() {} |
| 22 | + |
| 23 | + /** |
| 24 | + * Captures screenshot using Android Screenshot library and stores in the filesystem. |
| 25 | + * Special Cases: |
| 26 | + * If the screenshotName contains spaces or does not pass validation, the corresponding |
| 27 | + * screenshot is not visible on BrowserStack's Dashboard. |
| 28 | + * If there is any runtime exception while capturing screenshot, the method throws |
| 29 | + * Exception and the test might fail if exception is not handled properly. |
| 30 | + * @param screenshotName a screenshot identifier |
| 31 | + * @return path to the screenshot file |
| 32 | + */ |
| 33 | + public static String capture(String screenshotName) { |
| 34 | + StackTraceElement testClass = findTestClassTraceElement(Thread.currentThread().getStackTrace()); |
| 35 | + className = testClass.getClassName().replaceAll("[^A-Za-z0-9._-]", "_"); |
| 36 | + methodName = testClass.getMethodName(); |
| 37 | + EspressoScreenCaptureProcessor screenCaptureProcessor = new EspressoScreenCaptureProcessor(); |
| 38 | + |
| 39 | + if (!SCREENSHOT_NAME_VALIDATION.matcher(screenshotName).matches()) { |
| 40 | + throw new IllegalArgumentException("ScreenshotName must match " + SCREENSHOT_NAME_VALIDATION.pattern() + "."); |
| 41 | + } else { |
| 42 | + ScreenCapture capture = Screenshot.capture(); |
| 43 | + capture.setFormat(Bitmap.CompressFormat.PNG); |
| 44 | + capture.setName(screenshotName); |
| 45 | + |
| 46 | + try { |
| 47 | + return screenCaptureProcessor.process(capture); |
| 48 | + } catch (IOException e) { |
| 49 | + throw new RuntimeException("Unable to capture screenshot.", e); |
| 50 | + } |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + /** |
| 55 | + * Extracts the currently executing test's trace element based on the test runner |
| 56 | + * or any framework being used. |
| 57 | + * @param trace stacktrace of the currently running test |
| 58 | + * @return StackTrace Element corresponding to the current test being executed. |
| 59 | + */ |
| 60 | + private static StackTraceElement findTestClassTraceElement(StackTraceElement[] trace) { |
| 61 | + for(int i = trace.length - 1; i >= 0; --i) { |
| 62 | + StackTraceElement element = trace[i]; |
| 63 | + if ("android.test.InstrumentationTestCase".equals(element.getClassName()) && "runMethod".equals(element.getMethodName())) { |
| 64 | + return extractStackElement(trace, i); |
| 65 | + } |
| 66 | + |
| 67 | + if ("org.junit.runners.model.FrameworkMethod$1".equals(element.getClassName()) && "runReflectiveCall".equals(element.getMethodName())) { |
| 68 | + return extractStackElement(trace, i); |
| 69 | + } |
| 70 | + |
| 71 | + if ("cucumber.runtime.model.CucumberFeature".equals(element.getClassName()) && "run".equals(element.getMethodName())) { |
| 72 | + return extractStackElement(trace, i); |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + throw new IllegalArgumentException("Could not find test class!"); |
| 77 | + } |
| 78 | + |
| 79 | + /** |
| 80 | + * Based on the test runner or framework being used, extracts the exact traceElement. |
| 81 | + * @param trace stacktrace of the currently running test |
| 82 | + * @param i a reference index |
| 83 | + * @return trace element based on the index passed |
| 84 | + */ |
| 85 | + private static StackTraceElement extractStackElement(StackTraceElement[] trace, int i) { |
| 86 | + int testClassTraceIndex = Build.VERSION.SDK_INT >= 23 ? i - 2 : i - 3; |
| 87 | + return trace[testClassTraceIndex]; |
| 88 | + } |
| 89 | + |
| 90 | + private static class EspressoScreenCaptureProcessor extends BasicScreenCaptureProcessor { |
| 91 | + private static final String SCREENSHOT = "screenshot"; |
| 92 | + |
| 93 | + /** |
| 94 | + * There are 2 kinds of directories where screenshots can be stored: |
| 95 | + * Option 1: |
| 96 | + * Path: /storage/emulated/0/Android/data/<bundleId>/files/screenshots/<className> |
| 97 | + * Code snippet: |
| 98 | + * File screenshotDir = new File(String.valueOf(ApplicationProvider.getApplicationContext().getExternalFilesDir(null)), SCREENSHOT); |
| 99 | + * |
| 100 | + * Option 2: |
| 101 | + * Path: /storage/emulated/0/Download/screenshots |
| 102 | + * Code snippet: |
| 103 | + * File screenshotDir = new File(String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)), SCREENSHOT); |
| 104 | + */ |
| 105 | + EspressoScreenCaptureProcessor() { |
| 106 | + File screenshotDir = new File(String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)), SCREENSHOT); |
| 107 | + File classDir = new File(screenshotDir, className); |
| 108 | + mDefaultScreenshotPath = new File(classDir, methodName); |
| 109 | + } |
| 110 | + |
| 111 | + /** |
| 112 | + * Converts the filename to a standard path to be stored on device. |
| 113 | + * Example: "post_addition" converts to "1648038895211_post_addition" |
| 114 | + * which is later suffixed by the file extension i.e. png. |
| 115 | + * @param filename a screenshot identifier |
| 116 | + * @return custom filename format |
| 117 | + */ |
| 118 | + @Override |
| 119 | + protected String getFilename(String filename) { |
| 120 | + return System.currentTimeMillis() + "_" + filename; |
| 121 | + } |
| 122 | + } |
| 123 | +} |
0 commit comments