lov*_*219 3 automation screenshot kotlin android-espresso android-instrumentation
自从 Android 10 上改进的隐私更改Android 10 隐私更改以来,我注意到 Kotlin 中的屏幕截图失败测试观察程序规则(扩展了 Espresso BasicScreenCaptureProcessor)不再保存失败屏幕截图,因为我使用的是getExternalStoragePublicDirectoryAndroid 10 上已弃用的屏幕截图。
目前实现的概念非常类似于How to take Screenshot at the point where test failed in Espresso?
class TestScreenCaptureProcessor : BasicScreenCaptureProcessor() {
init {
this.mDefaultScreenshotPath = File(
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"Failure_Screenshots"
).absolutePath
)
}
Run Code Online (Sandbox Code Playgroud)
正如在其他帖子中看到的,我可以使用getInstrumentation().getTargetContext().getApplicationContext().getExternalFilesDir(DIRECTORY_PICTURES)
这会将文件存储在 -/sdcard/Android/data/your.package.name/files/Pictures目录中,但connectedAndroidTestgradle 任务最后会删除该应用程序以及上面列出的文件夹。
我想知道是否有其他人遇到过类似的情况,并考虑过在 Android 10 上存储故障屏幕截图的方法,存储在测试运行完成后不会被删除的位置以及 Espresso Instrumentation 测试可以访问的位置。
我的 UI 测试在各种设备上运行,因此需要一种通用的文件存储方式来适应所有模型。
经过大量研究,我找到了一种使用MediaStore在基于SDK版本的kotlin中保存屏幕截图的方法。
/**
* storeFailureScreenshot will store the bitmap based on the SDK level of the
* device. Due to security improvements and changes to how data can be accessed in
* SDK levels >=29 Failure screenshots will be stored in
* sdcard/DIRECTORY_PICTURES/Failure_Screenshots.
*/
fun storeFailureScreenshot(bitmap: Bitmap, screenshotName: String) {
val contentResolver = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext.contentResolver
// Check SDK version of device to determine how to save the screenshot.
if (android.os.Build.VERSION.SDK_INT >= 29) {
useMediaStoreScreenshotStorage(
contentValues,
contentResolver,
screenshotName,
SCREENSHOT_FOLDER_LOCATION,
bitmap
)
} else {
usePublicExternalScreenshotStorage(
contentValues,
contentResolver,
screenshotName,
SCREENSHOT_FOLDER_LOCATION,
bitmap
)
}
}
/**
* This will be used by devices with SDK versions >=29. This is to overcome scoped
* storage considerations now in the SDK version listed to help limit file
* clutter. A Uniform resource identifier (Uri) is used to insert bitmap into
* the gallery using the contentValues previously specified. The contentResolver
* provides application access to content model to access and publish data in a
* secure manner, using MediaStore collections to do so. Files will
* be stored in sdcard/Pictures
*/
private fun useMediaStoreScreenshotStorage(
contentValues: ContentValues,
contentResolver: ContentResolver,
screenshotName: String,
screenshotLocation: String,
bitmap: Bitmap
) {
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "$screenshotName.jpeg")
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + screenshotLocation)
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
contentResolver.openOutputStream(uri)?.let { saveScreenshotToStream(bitmap, it) }
contentResolver.update(uri, contentValues, null, null)
}
}
/**
* Method to access internal storage on a handset with SDK version below 29.
* Directory will be in sdcard/Pictures. Relevant sub directories will be created
* & screenshot will be stored as a .jpeg file.
*/
private fun usePublicExternalScreenshotStorage(
contentValues: ContentValues,
contentResolver: ContentResolver,
screenshotName: String,
screenshotLocation: String,
bitmap: Bitmap
) {
val directory = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES + screenshotLocation).toString())
if (!directory.exists()) {
directory.mkdirs()
}
val file = File(directory, "$screenshotName.jpeg")
saveScreenshotToStream(bitmap, FileOutputStream(file))
val values = contentValues
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
}
/**
* Assigns the assignments about the Image media including, image type & date
* taken. Content values are used so the contentResolver can interpret them. These
* are applied to the contentValues object.
*/
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
/**
* Compresses the bitmap object to a .jpeg image format using the specified
* OutputStream of bytes.
*/
private fun saveScreenshotToStream(bitmap: Bitmap, outputStream: OutputStream) {
outputStream.use {
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, it)
} catch (e: IOException) {
Timber.e("Screenshot was not stored at this time")
}
}
}
Run Code Online (Sandbox Code Playgroud)
与 TestWatcher 结合使用,对 UI 测试失败进行屏幕截图。然后按照规则将其添加到测试类中。
private val deviceLanguage = Locale.getDefault().language
/**
* Finds current date and time & is put into format of Wed-Mar-06-15:52:17.
*/
fun getDate(): String = SimpleDateFormat("EEE-MMMM-dd-HH:mm:ss").format(Date())
/**
* ScreenshotFailureRule overrides TestWatcher failed rule and instead takes a
* screenshot using the UI Automation takeScreenshot method and the
* storeFailureScreenshot to decide where to store the bitmap when a failure
* occurs.
*/
class ScreenshotFailureRule : TestWatcher() {
override fun failed(e: Throwable?, description: Description) {
val screenShotName = "$deviceLanguage-${description.methodName}-${getDate()}"
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
storeFailureScreenshot(bitmap, screenShotName)
}
}
Run Code Online (Sandbox Code Playgroud)
文件存储在sdcard/Pictures/Failure_Screenshots名称为
en-testMethodName-Day-Month-Date-HH_MM_SS
规则调用使用:
val screenshotFailureRule = ScreenshotFailureRule()
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1314 次 |
| 最近记录: |