diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/DocLocScreenshotCapturer.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/DocLocScreenshotCapturer.kt index 42d741321..4baac2013 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/DocLocScreenshotCapturer.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/DocLocScreenshotCapturer.kt @@ -1,6 +1,7 @@ package com.kaspersky.kaspresso.docloc import com.kaspersky.kaspresso.device.screenshots.screenshotmaker.ScreenshotMaker +import com.kaspersky.kaspresso.docloc.metadata.saver.MetadataSaver import com.kaspersky.kaspresso.files.resources.ResourceFilesProvider import com.kaspersky.kaspresso.internal.wait.wait import com.kaspersky.kaspresso.logger.UiTestLogger diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/activities/metadata/MetadataModels.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/MetadataModels.kt similarity index 86% rename from kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/activities/metadata/MetadataModels.kt rename to kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/MetadataModels.kt index 510a3276c..3b242a961 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/activities/metadata/MetadataModels.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/MetadataModels.kt @@ -1,4 +1,4 @@ -package com.kaspersky.kaspresso.device.activities.metadata +package com.kaspersky.kaspresso.docloc.metadata internal data class Metadata(val window: Window) diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/activities/metadata/ActivityMetadata.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/ActivityMetadataExtractor.kt similarity index 70% rename from kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/activities/metadata/ActivityMetadata.kt rename to kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/ActivityMetadataExtractor.kt index c61ce50c9..b3b43f27a 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/activities/metadata/ActivityMetadata.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/ActivityMetadataExtractor.kt @@ -1,4 +1,4 @@ -package com.kaspersky.kaspresso.device.activities.metadata +package com.kaspersky.kaspresso.docloc.metadata.extractor import android.app.Activity import android.content.res.Resources @@ -6,17 +6,25 @@ import android.view.View import android.widget.TextView import androidx.test.espresso.util.TreeIterables import com.google.android.material.appbar.CollapsingToolbarLayout +import com.kaspersky.kaspresso.device.activities.Activities +import com.kaspersky.kaspresso.docloc.metadata.LocalizedString +import com.kaspersky.kaspresso.docloc.metadata.Metadata +import com.kaspersky.kaspresso.docloc.metadata.Window import com.kaspersky.kaspresso.logger.UiTestLogger /** * The utility class to collect metadata from a window. */ -internal class ActivityMetadata( - private val logger: UiTestLogger -) { +internal class ActivityMetadataExtractor( + private val logger: UiTestLogger, + private val activities: Activities, +) : MetadataExtractor { - companion object { - private const val INDEX_SEPARATOR = '_' + private val metadataExtractorHelper = MetadataExtractorHelper() + + override fun getMetadata(): Metadata { + val activity = activities.getResumed() ?: throw RuntimeException("Failed to get current activity") + return getFromActivity(activity) } /** @@ -27,16 +35,15 @@ internal class ActivityMetadata( * @param activity activity to collect metadata from. * @return Metadata for the activity. */ - internal fun getFromActivity(activity: Activity): Metadata { - return getMetadata(activity) + private fun getFromActivity(activity: Activity): Metadata { + return createMetadata(activity) } - private fun getMetadata(activity: Activity): Metadata { + private fun createMetadata(activity: Activity): Metadata { with(activity.window.decorView) { - val localizedStrings = - resolveAmbiguous( - getLocalizedStrings(this) - ) + val localizedStrings = metadataExtractorHelper.resolveAmbiguous( + getLocalizedStrings(this) + ) val window = Window( left, top, @@ -91,20 +98,4 @@ internal class ActivityMetadata( "[id:${Integer.toHexString(layout.id)}]" } } - - private fun resolveAmbiguous(localizedStrings: List): List { - return localizedStrings.groupBy { it.locValueDescription } - .values - .flatMap { groupedById -> - if (groupedById.size == 1) groupedById else addIndexes( - groupedById - ) - } - } - - private fun addIndexes(groupedById: List): List { - return groupedById.mapIndexed { index, locString -> - locString.copy(locValueDescription = "${locString.locValueDescription}$INDEX_SEPARATOR${index + 1}") - } - } } diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/MetadataExtractor.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/MetadataExtractor.kt new file mode 100644 index 000000000..34c3e61b4 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/MetadataExtractor.kt @@ -0,0 +1,7 @@ +package com.kaspersky.kaspresso.docloc.metadata.extractor + +import com.kaspersky.kaspresso.docloc.metadata.Metadata + +internal interface MetadataExtractor { + fun getMetadata(): Metadata +} diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/MetadataExtractorHelper.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/MetadataExtractorHelper.kt new file mode 100644 index 000000000..5fc517b2c --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/MetadataExtractorHelper.kt @@ -0,0 +1,25 @@ +package com.kaspersky.kaspresso.docloc.metadata.extractor + +import com.kaspersky.kaspresso.docloc.metadata.LocalizedString + +internal class MetadataExtractorHelper { + fun resolveAmbiguous(localizedStrings: List): List { + return localizedStrings.groupBy { it.locValueDescription } + .values + .flatMap { groupedById -> + if (groupedById.size == 1) groupedById else addIndexes( + groupedById + ) + } + } + + private fun addIndexes(groupedById: List): List { + return groupedById.mapIndexed { index, locString -> + locString.copy(locValueDescription = "${locString.locValueDescription}$INDEX_SEPARATOR${index + 1}") + } + } + + companion object { + private const val INDEX_SEPARATOR = '_' + } +} diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/UiMetadataExtractor.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/UiMetadataExtractor.kt new file mode 100644 index 000000000..23c95d8ac --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/extractor/UiMetadataExtractor.kt @@ -0,0 +1,102 @@ +package com.kaspersky.kaspresso.docloc.metadata.extractor + +import android.app.Activity +import androidx.test.uiautomator.By +import androidx.test.uiautomator.StaleObjectException +import androidx.test.uiautomator.UiDevice +import com.kaspersky.kaspresso.device.activities.Activities +import com.kaspersky.kaspresso.docloc.metadata.LocalizedString +import com.kaspersky.kaspresso.docloc.metadata.Metadata +import com.kaspersky.kaspresso.docloc.metadata.Window +import com.kaspersky.kaspresso.logger.UiTestLogger + +internal class UiMetadataExtractor( + private val uiDevice: UiDevice, + private val activities: Activities, + private val logger: UiTestLogger, +) : MetadataExtractor { + + private val metadataExtractorHelper = MetadataExtractorHelper() + + override fun getMetadata(): Metadata { + val activity = activities.getResumed() ?: throw RuntimeException("Failed to get current activity") + return createMetadata(activity) + } + + private fun createMetadata(activity: Activity): Metadata { + val localizedStrings = metadataExtractorHelper.resolveAmbiguous( + getLocalizedStrings() + ) + val window = activity.window.decorView.run { + Window( + left, + top, + width, + height, + localizedStrings + ) + } + return Metadata(window) + } + + private fun getLocalizedStrings(): List { + uiDevice.setCompressedLayoutHierarchy(false) + val objectsWithText = uiDevice.findObjects(By.enabled(true)) + .filterNotNull() + .filter { + try { + (it.resourceName != null && it.children.any { !it.text.isNullOrBlank() }) || + (!it.text.isNullOrBlank() && it.resourceName != null) + } catch (ex: StaleObjectException) { + logger.e("UiMetadataExtractor::getLocalizedStrings() - Error while loading object") + false + } + } + + val localizedStrings = mutableListOf() + /* + There might be a case when the text itself won't have an ID, but rather the text container will. + For example if we set an id to the button, Ui automator will see it as a container with an id and a child - text view without an id + + Probably buggy. Need better solution. + */ + for (obj in objectsWithText) { + try { + val resName = obj.resourceName ?: continue + if (obj.text.isNullOrBlank()) { // the text container + obj.children + .filter { !it.text.isNullOrBlank() } + .forEach { + val coords = it.visibleBounds + localizedStrings.add( + LocalizedString( + text = it.text, + locValueDescription = resName, + left = coords.left, + top = coords.top, + width = coords.width(), + height = coords.height(), + ) + ) + } + } else { // the text itself + val coords = obj.visibleBounds + localizedStrings.add( + LocalizedString( + text = obj.text, + locValueDescription = resName, + left = coords.left, + top = coords.top, + width = coords.width(), + height = coords.height(), + ) + ) + } + } catch (ex: StaleObjectException) { + logger.e("UiMetadataExtractor::getLocalizedStrings() - error while processing found objects") + } + } + + return localizedStrings + } +} diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/MetadataSaver.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/saver/DefaultMetadataSaver.kt similarity index 60% rename from kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/MetadataSaver.kt rename to kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/saver/DefaultMetadataSaver.kt index bb08cdbda..daa6eae94 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/MetadataSaver.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/saver/DefaultMetadataSaver.kt @@ -1,29 +1,28 @@ -package com.kaspersky.kaspresso.docloc +package com.kaspersky.kaspresso.docloc.metadata.saver import com.kaspersky.kaspresso.device.activities.Activities -import com.kaspersky.kaspresso.device.activities.metadata.ActivityMetadata import com.kaspersky.kaspresso.device.apps.Apps +import com.kaspersky.kaspresso.docloc.metadata.extractor.MetadataExtractor import com.kaspersky.kaspresso.internal.extensions.other.safeWrite import com.kaspersky.kaspresso.internal.extensions.other.toXml import com.kaspersky.kaspresso.logger.UiTestLogger import java.io.File -internal class MetadataSaver( +internal class DefaultMetadataSaver( private val activities: Activities, private val apps: Apps, - private val logger: UiTestLogger -) { - private val activityMetadata = ActivityMetadata(logger) + private val logger: UiTestLogger, + private val metadataExtractor: MetadataExtractor, +) : MetadataSaver { - fun saveScreenshotMetadata(folderPath: File, name: String) { + override fun saveScreenshotMetadata(folderPath: File, name: String) { val activity = activities.getResumed() if (activity == null) { logger.e("Activity is null when saving metadata $name") return } runCatching { - val metadata = activityMetadata.getFromActivity(activity) - .toXml(apps.targetAppPackageName) + val metadata = metadataExtractor.getMetadata().toXml(apps.targetAppPackageName) folderPath.resolve("$name.xml").safeWrite(logger, metadata) } } diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/saver/MetadataSaver.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/saver/MetadataSaver.kt new file mode 100644 index 000000000..b432d7bfa --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/docloc/metadata/saver/MetadataSaver.kt @@ -0,0 +1,7 @@ +package com.kaspersky.kaspresso.docloc.metadata.saver + +import java.io.File + +interface MetadataSaver { + fun saveScreenshotMetadata(folderPath: File, name: String) +} diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/extensions/other/MetadataModelsExt.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/extensions/other/MetadataModelsExt.kt index eaebb4f64..ab4a53c84 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/extensions/other/MetadataModelsExt.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/extensions/other/MetadataModelsExt.kt @@ -1,8 +1,8 @@ package com.kaspersky.kaspresso.internal.extensions.other import android.text.TextUtils.htmlEncode -import com.kaspersky.kaspresso.device.activities.metadata.LocalizedString -import com.kaspersky.kaspresso.device.activities.metadata.Metadata +import com.kaspersky.kaspresso.docloc.metadata.LocalizedString +import com.kaspersky.kaspresso.docloc.metadata.Metadata /** * Transforms [Metadata] object to an xml string. diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/params/ScreenshotParams.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/params/ScreenshotParams.kt index 7610ef36f..b81bb45e7 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/params/ScreenshotParams.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/params/ScreenshotParams.kt @@ -2,7 +2,21 @@ package com.kaspersky.kaspresso.params /** * @param quality quality of the PNG compression; range: 0-100 + * @param metadataExtractor determines the API used to get metadata from the app */ class ScreenshotParams( - val quality: Int = 100 + val quality: Int = 100, + val metadataExtractor: MetadataExtractors = MetadataExtractors.Default, ) + +enum class MetadataExtractors { + /** + * Traverses XML's views hierarchy to get views metadata + */ + Default, + + /** + * Dumps and traverses UI automator tree to get views metadata. Recommended to use for Compose screens + */ + UiAutomator, +} diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/testcases/api/testcase/DocLocScreenshotTestCase.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/testcases/api/testcase/DocLocScreenshotTestCase.kt index 7a2e4a311..e96e52ed9 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/testcases/api/testcase/DocLocScreenshotTestCase.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/testcases/api/testcase/DocLocScreenshotTestCase.kt @@ -12,7 +12,9 @@ import com.kaspersky.kaspresso.device.screenshots.screenshotmaker.DocLocScreensh import com.kaspersky.kaspresso.device.screenshots.screenshotmaker.ExternalScreenshotMaker import com.kaspersky.kaspresso.device.screenshots.screenshotmaker.InternalScreenshotMaker import com.kaspersky.kaspresso.docloc.DocLocScreenshotCapturer -import com.kaspersky.kaspresso.docloc.MetadataSaver +import com.kaspersky.kaspresso.docloc.metadata.extractor.ActivityMetadataExtractor +import com.kaspersky.kaspresso.docloc.metadata.extractor.UiMetadataExtractor +import com.kaspersky.kaspresso.docloc.metadata.saver.DefaultMetadataSaver import com.kaspersky.kaspresso.docloc.rule.LocaleRule import com.kaspersky.kaspresso.docloc.rule.ToggleNightModeRule import com.kaspersky.kaspresso.files.dirs.DefaultDirsProvider @@ -33,6 +35,7 @@ import com.kaspersky.kaspresso.internal.extensions.other.getAllInterfaces import com.kaspersky.kaspresso.internal.invocation.UiInvocationHandler import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.logger.UiTestLogger +import com.kaspersky.kaspresso.params.MetadataExtractors import com.kaspersky.kaspresso.params.ScreenshotParams import org.junit.Before import org.junit.Rule @@ -180,7 +183,15 @@ abstract class DocLocScreenshotTestCase( ), fullWindowScreenshotMaker = InternalScreenshotMaker(kaspresso.device.activities, screenshotParams) ), - metadataSaver = MetadataSaver(kaspresso.device.activities, kaspresso.device.apps, logger) + metadataSaver = DefaultMetadataSaver( + kaspresso.device.activities, + kaspresso.device.apps, + logger, + metadataExtractor = when (screenshotParams.metadataExtractor) { + MetadataExtractors.Default -> ActivityMetadataExtractor(logger, kaspresso.device.activities) + MetadataExtractors.UiAutomator -> UiMetadataExtractor(kaspresso.device.uiDevice, kaspresso.device.activities, logger) + } + ) ) }