diff --git a/kaspresso/src/main/java/androidx/test/espresso/assertion/ViewAssertionExt.kt b/kaspresso/src/main/java/androidx/test/espresso/assertion/ViewAssertionExt.kt index f2273ab0e..efce82dcc 100644 --- a/kaspresso/src/main/java/androidx/test/espresso/assertion/ViewAssertionExt.kt +++ b/kaspresso/src/main/java/androidx/test/espresso/assertion/ViewAssertionExt.kt @@ -1,13 +1,15 @@ package androidx.test.espresso.assertion +import android.view.View import androidx.test.espresso.ViewAssertion +import org.hamcrest.Matcher import org.hamcrest.StringDescription /** * @return a [String] description of [ViewAssertion]. */ internal fun ViewAssertion?.describe(): String { - if (this == null) return "null" + this ?: return "null" val builder = StringBuilder("Check ") @@ -21,4 +23,9 @@ internal fun ViewAssertion?.describe(): String { } return builder.toString() +} + +internal fun ViewAssertion.getViewMatcher(): Matcher? { + if (this is ViewAssertions.MatchesViewAssertion) return viewMatcher + return null } \ No newline at end of file diff --git a/kaspresso/src/main/java/androidx/test/espresso/assertion/ViewAssertionsAndMatcherProvider.kt b/kaspresso/src/main/java/androidx/test/espresso/assertion/ViewAssertionsAndMatcherProvider.kt new file mode 100644 index 000000000..87f055205 --- /dev/null +++ b/kaspresso/src/main/java/androidx/test/espresso/assertion/ViewAssertionsAndMatcherProvider.kt @@ -0,0 +1,12 @@ +package androidx.test.espresso.assertion + +import androidx.test.espresso.ViewAssertion +import androidx.test.espresso.matcher.ViewMatchersProvider + +abstract class ViewAssertionsAndMatcherProvider : ViewMatchersProvider() { + + protected val MATCHES_VIEW_ASSERTION: Class = + ViewAssertions.MatchesViewAssertion::class.java + + // Here will be more view assertions +} \ No newline at end of file diff --git a/kaspresso/src/main/java/androidx/test/espresso/matcher/ViewMatchersProvider.kt b/kaspresso/src/main/java/androidx/test/espresso/matcher/ViewMatchersProvider.kt new file mode 100644 index 000000000..2d9756b84 --- /dev/null +++ b/kaspresso/src/main/java/androidx/test/espresso/matcher/ViewMatchersProvider.kt @@ -0,0 +1,15 @@ +package androidx.test.espresso.matcher + +import android.view.View +import org.hamcrest.Matcher + +abstract class ViewMatchersProvider { + + protected val WITH_TEXT_MATCHER: Class> = + ViewMatchers.WithTextMatcher::class.java + + protected val WITH_EFFECTIVE_VISIBILITY: Class> = + ViewMatchers.WithEffectiveVisibilityMatcher::class.java + + // Here will be more view matchers +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/ComposeProviderImpl.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/ComposeProviderImpl.kt index 0bb15776e..346c30d5b 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/ComposeProviderImpl.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/ComposeProviderImpl.kt @@ -39,9 +39,9 @@ class ComposeProviderImpl( val (flakySafetyProvider, failureLoggingProvider) = getProviders() - failureLoggingProvider.withLoggingOnFailureIfNotNull { + failureLoggingProvider.withLoggingOnFailureIfNotNull(null) { flakySafetyProvider.flakySafelyIfNotNull { - invokeComposed(actions, kaspresso.libLogger) + invokeComposed(actions, kaspresso.libLogger, elements) } } @@ -63,7 +63,7 @@ class ComposeProviderImpl( val (flakySafetyProvider, failureLoggingProvider) = getProviders() - failureLoggingProvider.withLoggingOnFailureIfNotNull { + failureLoggingProvider.withLoggingOnFailureIfNotNull(view.interaction) { flakySafetyProvider.flakySafelyIfNotNull { invokeComposed(actions, kaspresso.libLogger) } @@ -72,9 +72,9 @@ class ComposeProviderImpl( setInterception() } - private fun getProviders(): Pair { + private fun getProviders(): Pair?> { var flakySafetyProvider: FlakySafetyProvider? = null - var failureLoggingProvider: FailureLoggingProvider? = null + var failureLoggingProvider: FailureLoggingProvider? = null kaspresso.viewBehaviorInterceptors.forEach { viewBehaviorInterceptor: ViewBehaviorInterceptor -> when (viewBehaviorInterceptor) { diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/InvokeComposed.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/InvokeComposed.kt index c3a2b7505..5e7ae8f6f 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/InvokeComposed.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/InvokeComposed.kt @@ -1,5 +1,7 @@ package com.kaspersky.kaspresso.compose +import com.agoda.kakao.intercept.Interceptable +import com.kaspersky.kaspresso.internal.exceptions.ThrowableWithInteraction import com.kaspersky.kaspresso.logger.UiTestLogger import io.reactivex.exceptions.ExtCompositeException @@ -9,17 +11,21 @@ import io.reactivex.exceptions.ExtCompositeException * @param actions the list of actions to invoke. * @param logger the logger */ -internal fun invokeComposed(actions: List<() -> Unit>, logger: UiTestLogger) { +internal fun invokeComposed( + actions: List<() -> Unit>, + logger: UiTestLogger, + elements: List>? = null +) { logger.i("Composed action started.") val cachedErrors: MutableList = mutableListOf() - actions.forEach { action: () -> Unit -> + actions.forEachIndexed { i: Int, action: () -> Unit -> try { action.invoke() logger.i("Composed action succeeded.") return } catch (error: Throwable) { - cachedErrors += error + cachedErrors += elements?.let { ThrowableWithInteraction(error, it[i].view.interaction as Any) } ?: error logger.i("One part of composed action failed.") } } diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/WebComposeProviderImpl.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/WebComposeProviderImpl.kt index 210764965..5b7bdd899 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/WebComposeProviderImpl.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/WebComposeProviderImpl.kt @@ -1,5 +1,6 @@ package com.kaspersky.kaspresso.compose +import androidx.test.espresso.web.sugar.Web import com.agoda.kakao.web.WebElementBuilder import com.kaspersky.kaspresso.compose.pack.ActionsOnWebElementsPack import com.kaspersky.kaspresso.compose.pack.ActionsPack @@ -34,7 +35,7 @@ class WebComposeProviderImpl( val (flakySafetyProvider, failureLoggingProvider) = getProviders() - failureLoggingProvider.withLoggingOnFailureIfNotNull { + failureLoggingProvider.withLoggingOnFailureIfNotNull(null) { flakySafetyProvider.flakySafelyIfNotNull { invokeComposed(actions, kaspresso.libLogger) } @@ -59,7 +60,7 @@ class WebComposeProviderImpl( val (flakySafetyProvider, failureLoggingProvider) = getProviders() - failureLoggingProvider.withLoggingOnFailureIfNotNull { + failureLoggingProvider.withLoggingOnFailureIfNotNull(web.interaction) { flakySafetyProvider.flakySafelyIfNotNull { invokeComposed(actions, kaspresso.libLogger) } @@ -68,9 +69,9 @@ class WebComposeProviderImpl( webElementBuilder.setInterception() } - private fun getProviders(): Pair { + private fun getProviders(): Pair>?> { var flakySafetyProvider: FlakySafetyProvider? = null - var failureLoggingProvider: FailureLoggingProvider? = null + var failureLoggingProvider: FailureLoggingProvider>? = null kaspresso.webBehaviorInterceptors.forEach { webBehaviorInterceptor: WebBehaviorInterceptor -> when (webBehaviorInterceptor) { diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/pack/ActionsOnElementsPack.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/pack/ActionsOnElementsPack.kt index 06d1f5b71..02dc2b501 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/pack/ActionsOnElementsPack.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/compose/pack/ActionsOnElementsPack.kt @@ -12,7 +12,6 @@ import java.lang.IllegalArgumentException * The builder class for parameters of [com.kaspersky.kaspresso.compose.ComposeProvider.compose] method. */ class ActionsOnElementsPack { - private val elements: MutableList> = mutableListOf() private val actions: MutableList<() -> Unit> = mutableListOf() diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingParams.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingParams.kt new file mode 100644 index 000000000..3fedeeae8 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingParams.kt @@ -0,0 +1,38 @@ +package com.kaspersky.kaspresso.failure + +import android.view.View +import androidx.test.espresso.ViewAssertion +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.assertion.ViewAssertionsAndMatcherProvider +import com.kaspersky.kaspresso.obtain_value.actions.ObtainTextViewAction +import com.kaspersky.kaspresso.obtain_value.atom.ObtainValueAtom +import com.kaspersky.kaspresso.obtain_value.actions.ObtainValueViewAction +import com.kaspersky.kaspresso.obtain_value.actions.ObtainVisibilityViewAction +import org.hamcrest.Matcher + +data class FailureLoggingParams internal constructor( + var viewAssertionDescribers: MutableMap, ViewInteraction.() -> String>, + var viewMatcherDescribers: MutableMap>, () -> ObtainValueViewAction>, + var webMatcherDescribers: MutableMap>, () -> ObtainValueAtom<*>> +) : ViewAssertionsAndMatcherProvider() { + + internal constructor() : this( + viewAssertionDescribers = hashMapOf(), + viewMatcherDescribers = hashMapOf(), + webMatcherDescribers = hashMapOf() + ) { + viewMatcherDescribers = hashMapOf( + WITH_TEXT_MATCHER to { ObtainTextViewAction() }, + WITH_EFFECTIVE_VISIBILITY to { ObtainVisibilityViewAction() } + // here will be more view matchers + // users can add their own + ) + + // here will be webMatcherDescribers default initialization + + // here will be viewAssertionDescribers default initialization +// viewAssertionDescribers = hashMapOf( +// MATCHES_VIEW_ASSERTION to { ObtainTextViewAction().apply { perform(this) }.value } +// ) + } +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingProvider.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingProvider.kt index 14d6d9260..206338c55 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingProvider.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingProvider.kt @@ -6,7 +6,7 @@ import org.hamcrest.Matcher /** * An interface to provide the logging failures functionality. */ -interface FailureLoggingProvider { +interface FailureLoggingProvider { /** * Invokes the given [action] and logs if it fails. @@ -15,14 +15,14 @@ interface FailureLoggingProvider { * * @return the result of the [action] invocation. */ - fun withLoggingOnFailure(action: () -> T): T + fun withLoggingOnFailure(interaction: Interaction?, action: () -> T): T /** * Logs the [error]'s stacktrace. * * @param error the error to get stacktrace from. */ - fun logStackTrace(error: Throwable) + fun logStackTrace(interaction: Interaction?, error: Throwable) /** * Logs the [error] description got by [viewMatcher] and throws the [error]. @@ -40,5 +40,7 @@ interface FailureLoggingProvider { * * @return the result of the [action] invocation. */ -internal fun FailureLoggingProvider?.withLoggingOnFailureIfNotNull(action: () -> T): T = - if (this != null) withLoggingOnFailure(action) else action.invoke() \ No newline at end of file +internal fun FailureLoggingProvider?.withLoggingOnFailureIfNotNull( + interaction: Interaction?, + action: () -> T +): T = if (this != null) withLoggingOnFailure(interaction, action) else action.invoke() \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingProviderImpl.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingProviderImpl.kt index 108c9dced..5b0a4ab71 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingProviderImpl.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/FailureLoggingProviderImpl.kt @@ -2,9 +2,12 @@ package com.kaspersky.kaspresso.failure import android.view.View import androidx.test.espresso.PerformException +import com.kaspersky.kaspresso.internal.exceptions.ThrowableWithInteraction import com.kaspersky.kaspresso.internal.extensions.espressoext.describe +import com.kaspersky.kaspresso.internal.extensions.other.getException import com.kaspersky.kaspresso.internal.extensions.other.getStackTraceAsString import com.kaspersky.kaspresso.logger.UiTestLogger +import com.kaspersky.kaspresso.failure.describe.FailedAssertionDescriber import io.reactivex.exceptions.ExtCompositeException import junit.framework.AssertionFailedError import org.hamcrest.Matcher @@ -12,9 +15,10 @@ import org.hamcrest.Matcher /** * The implementation of the [FailureLoggingProvider] interface. */ -class FailureLoggingProviderImpl( - private val logger: UiTestLogger -) : FailureLoggingProvider { +class FailureLoggingProviderImpl( + private val logger: UiTestLogger, + private val failedAssertionDescriber: FailedAssertionDescriber? = null +) : FailureLoggingProvider { /** * Invokes the given [action] and logs if it fails. @@ -26,12 +30,23 @@ class FailureLoggingProviderImpl( * @throws Throwable if the [action] invocation failed. */ @Throws(Throwable::class) - override fun withLoggingOnFailure(action: () -> T): T { + override fun withLoggingOnFailure(interaction: Interaction?, action: () -> T): T { return try { action.invoke() } catch (error: Throwable) { - logStackTrace(error) - throw error + logStackTrace(interaction, error) + + throw when (error) { + is ThrowableWithInteraction -> { + error.throwable + } + is ExtCompositeException -> { + error.exceptions.map { if (it is ThrowableWithInteraction) it.throwable else it }.getException()!! + } + else -> { + error + } + } } } @@ -40,13 +55,43 @@ class FailureLoggingProviderImpl( * * @param error the error to get stacktrace from. */ - override fun logStackTrace(error: Throwable) { - logger.e(error.getStackTraceAsString()) - - if (error is ExtCompositeException) { - error.exceptions.forEachIndexed { i: Int, e: Throwable -> - logger.e("Composed exception ${i + 1} :") - logger.e(e.getStackTraceAsString()) + override fun logStackTrace(interaction: Interaction?, error: Throwable) { + when (error) { + is ExtCompositeException -> { + logger.e(error.getStackTraceAsString()) + error.exceptions.forEachIndexed { i: Int, e: Throwable -> + logger.e("Composed exception ${i + 1} :") + if (e is ThrowableWithInteraction) { + if (e.throwable is AssertionError) { + failedAssertionDescriber?.getDescription(e.interaction as Interaction) + ?.let { + logger.e( + e.throwable.getStackTraceAsString() + .replace(e.throwable.toString(), "${e.throwable.javaClass.name}: $it") + ) + } + ?: logger.e(e.throwable.getStackTraceAsString()) + } else { + logger.e(e.throwable.getStackTraceAsString()) + } + } else { + logger.e(e.getStackTraceAsString()) + } + } + } + else -> { + if (interaction != null && error is AssertionError) { + failedAssertionDescriber?.getDescription(interaction) + ?.let { + logger.e( + error.getStackTraceAsString() + .replace(error.toString(), "${error.javaClass.name}: $it") + ) + } + ?: logger.e(error.getStackTraceAsString()) + } else { + logger.e(error.getStackTraceAsString()) + } } } } diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/LoggingFailureHandler.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/LoggingFailureHandler.kt index 320dfe621..17e3ef8b8 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/LoggingFailureHandler.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/LoggingFailureHandler.kt @@ -2,6 +2,7 @@ package com.kaspersky.kaspresso.failure import android.view.View import androidx.test.espresso.FailureHandler +import androidx.test.espresso.ViewInteraction import com.kaspersky.kaspresso.logger.UiTestLogger import org.hamcrest.Matcher @@ -11,7 +12,7 @@ import org.hamcrest.Matcher class LoggingFailureHandler( logger: UiTestLogger ) : FailureHandler, - FailureLoggingProvider by FailureLoggingProviderImpl(logger) { + FailureLoggingProvider by FailureLoggingProviderImpl(logger) { /** * Calls [logDescriptionAndThrow] on each failure. diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/AssertionCache.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/AssertionCache.kt new file mode 100644 index 000000000..c09a6bf68 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/AssertionCache.kt @@ -0,0 +1,11 @@ +package com.kaspersky.kaspresso.failure.describe + +import android.view.View +import androidx.test.espresso.ViewAssertion +import org.hamcrest.Matcher + +data class AssertionCache( + internal var cachedViewAssertion: Class? = null, + internal var cachedViewMatcher: Matcher? = null, + internal var cachedWebMatcher: Matcher<*>? = null +) \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedAssertionDescriber.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedAssertionDescriber.kt new file mode 100644 index 000000000..d671c9e8d --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedAssertionDescriber.kt @@ -0,0 +1,5 @@ +package com.kaspersky.kaspresso.failure.describe + +interface FailedAssertionDescriber { + fun getDescription(interaction: Interaction): String +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedViewAssertionDescriber.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedViewAssertionDescriber.kt new file mode 100644 index 000000000..5bcdc6991 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedViewAssertionDescriber.kt @@ -0,0 +1,40 @@ +package com.kaspersky.kaspresso.failure.describe + +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.assertion.ViewAssertionsAndMatcherProvider +import com.kaspersky.kaspresso.failure.FailureLoggingParams +import com.kaspersky.kaspresso.internal.extensions.espressoext.describe + +class FailedViewAssertionDescriber( + private val failureLoggingParams: FailureLoggingParams, + private val assertionCache: AssertionCache +) : ViewAssertionsAndMatcherProvider(), + FailedAssertionDescriber { + + override fun getDescription(interaction: ViewInteraction): String { + val actualValue: String? = with(failureLoggingParams) { + with(assertionCache) { + when (cachedViewAssertion) { + MATCHES_VIEW_ASSERTION -> { + if (cachedViewMatcher == null) { + null + } else { + viewMatcherDescribers[cachedViewMatcher!!.javaClass] + ?.invoke() + ?.apply { interaction.perform(this) } + ?.value + } + } + // here will be more view assertions + else -> { + viewAssertionDescribers[cachedViewAssertion]?.invoke(interaction) + } + } + } + } + + return "assertion on selected view failed\n" + + "Expected: ${assertionCache.cachedViewMatcher?.describe() ?: "[UNDEFINED]"}\n" + + "Got: ${actualValue ?: "[UNDEFINED]"}" + } +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedWebAssertionDescriber.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedWebAssertionDescriber.kt new file mode 100644 index 000000000..cac2238b3 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/failure/describe/FailedWebAssertionDescriber.kt @@ -0,0 +1,14 @@ +package com.kaspersky.kaspresso.failure.describe + +import androidx.test.espresso.web.sugar.Web +import com.kaspersky.kaspresso.failure.FailureLoggingParams + +class FailedWebAssertionDescriber( + private val failureLoggingParams: FailureLoggingParams, + private val assertionCache: AssertionCache +) : FailedAssertionDescriber> { + + override fun getDescription(interaction: Web.WebInteraction<*>): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/flakysafety/FlakySafetyParams.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/flakysafety/FlakySafetyParams.kt index 15e4f20f6..b9ac693b5 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/flakysafety/FlakySafetyParams.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/flakysafety/FlakySafetyParams.kt @@ -2,6 +2,7 @@ package com.kaspersky.kaspresso.flakysafety import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.PerformException +import com.kaspersky.kaspresso.internal.exceptions.ThrowableWithInteraction /** * The class that holds all the necessary for [FlakySafetyProvider] parameters. @@ -17,7 +18,8 @@ class FlakySafetyParams( mutableSetOf( PerformException::class.java, NoMatchingViewException::class.java, - AssertionError::class.java + AssertionError::class.java, + ThrowableWithInteraction::class.java ) ) { private companion object { diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingDataBehaviorInterceptor.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingDataBehaviorInterceptor.kt index 13368de33..a65e63a5c 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingDataBehaviorInterceptor.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingDataBehaviorInterceptor.kt @@ -13,7 +13,7 @@ import com.kaspersky.kaspresso.logger.UiTestLogger class FailureLoggingDataBehaviorInterceptor( logger: UiTestLogger ) : DataBehaviorInterceptor, - FailureLoggingProvider by FailureLoggingProviderImpl(logger) { + FailureLoggingProvider by FailureLoggingProviderImpl(logger) { /** * Wraps the given [action] invocation with the failure logging. @@ -21,5 +21,6 @@ class FailureLoggingDataBehaviorInterceptor( * @param interaction the intercepted [DataInteraction]. * @param action the action to invoke. */ - override fun intercept(interaction: DataInteraction, action: () -> T): T = withLoggingOnFailure(action) + override fun intercept(interaction: DataInteraction, action: () -> T): T = + withLoggingOnFailure(interaction, action) } \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingViewBehaviorInterceptor.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingViewBehaviorInterceptor.kt index 57e426bfc..79728cc7a 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingViewBehaviorInterceptor.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingViewBehaviorInterceptor.kt @@ -5,15 +5,17 @@ import com.kaspersky.kaspresso.failure.FailureLoggingProvider import com.kaspersky.kaspresso.failure.FailureLoggingProviderImpl import com.kaspersky.kaspresso.interceptors.behavior.ViewBehaviorInterceptor import com.kaspersky.kaspresso.logger.UiTestLogger +import com.kaspersky.kaspresso.failure.describe.FailedViewAssertionDescriber /** * The implementation of [ViewBehaviorInterceptor] and [FailureLoggingProvider] interfaces. * Provides failure logging functionality for [ViewInteraction.perform] and [ViewInteraction.check] calls. */ class FailureLoggingViewBehaviorInterceptor( - logger: UiTestLogger + logger: UiTestLogger, + failedViewAssertionDescriber: FailedViewAssertionDescriber? = null ) : ViewBehaviorInterceptor, - FailureLoggingProvider by FailureLoggingProviderImpl(logger) { + FailureLoggingProvider by FailureLoggingProviderImpl(logger, failedViewAssertionDescriber) { /** * Wraps the given [action] invocation with the failure logging. @@ -21,5 +23,6 @@ class FailureLoggingViewBehaviorInterceptor( * @param interaction the intercepted [ViewInteraction]. * @param action the action to invoke. */ - override fun intercept(interaction: ViewInteraction, action: () -> T): T = withLoggingOnFailure(action) + override fun intercept(interaction: ViewInteraction, action: () -> T): T = + withLoggingOnFailure(interaction, action) } \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingWebBehaviorInterceptor.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingWebBehaviorInterceptor.kt index abd1760a6..cd524370e 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingWebBehaviorInterceptor.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/behavior/impl/failure/FailureLoggingWebBehaviorInterceptor.kt @@ -5,15 +5,17 @@ import com.kaspersky.kaspresso.failure.FailureLoggingProvider import com.kaspersky.kaspresso.failure.FailureLoggingProviderImpl import com.kaspersky.kaspresso.interceptors.behavior.WebBehaviorInterceptor import com.kaspersky.kaspresso.logger.UiTestLogger +import com.kaspersky.kaspresso.failure.describe.FailedWebAssertionDescriber /** * The implementation of [WebBehaviorInterceptor] and [FailureLoggingProvider] interfaces. * Provides failure logging functionality for [Web.WebInteraction.perform] and [Web.WebInteraction.check] calls. */ class FailureLoggingWebBehaviorInterceptor( - logger: UiTestLogger + logger: UiTestLogger, + failedWebAssertionDescriber: FailedWebAssertionDescriber? = null ) : WebBehaviorInterceptor, - FailureLoggingProvider by FailureLoggingProviderImpl(logger) { + FailureLoggingProvider> by FailureLoggingProviderImpl(logger, failedWebAssertionDescriber) { /** * Wraps the given [action] invocation with the failure logging. @@ -21,5 +23,6 @@ class FailureLoggingWebBehaviorInterceptor( * @param interaction the intercepted [Web.WebInteraction]. * @param action the action to invoke. */ - override fun intercept(interaction: Web.WebInteraction<*>, action: () -> T): T = withLoggingOnFailure(action) + override fun intercept(interaction: Web.WebInteraction<*>, action: () -> T): T = + withLoggingOnFailure(interaction, action) } \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/watcher/view/impl/caching/CachingViewAssertionWatcherInterceptor.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/watcher/view/impl/caching/CachingViewAssertionWatcherInterceptor.kt new file mode 100644 index 000000000..f0f990ef8 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/watcher/view/impl/caching/CachingViewAssertionWatcherInterceptor.kt @@ -0,0 +1,30 @@ +package com.kaspersky.kaspresso.interceptors.watcher.view.impl.caching + +import android.view.View +import androidx.test.espresso.NoMatchingViewException +import androidx.test.espresso.ViewAssertion +import androidx.test.espresso.assertion.getViewMatcher +import com.kaspersky.kaspresso.failure.describe.AssertionCache +import com.kaspersky.kaspresso.interceptors.watcher.view.ViewAssertionWatcherInterceptor + +/** + * The implementation of [ViewAssertionWatcherInterceptor] that obtains info about executing [ViewAssertion] for + * failure logging. + */ +class CachingViewAssertionWatcherInterceptor( + private val assertionCache: AssertionCache +) : ViewAssertionWatcherInterceptor { + + /** + * Stores [viewAssertion] and it's [org.hamcrest.Matcher] to [AssertionCache] for + * [com.kaspersky.kaspresso.failure.FailureLoggingProvider] usage. + * + * @param viewAssertion responsible for performing assertions on a [View] element. + * @param view an Android [View], on which [viewAssertion] is performed. + * @param exception indicates that a given matcher did not match any elements in the view hierarchy. + */ + override fun intercept(viewAssertion: ViewAssertion, view: View?, exception: NoMatchingViewException?) { + assertionCache.cachedViewAssertion = viewAssertion.javaClass + assertionCache.cachedViewMatcher = viewAssertion.getViewMatcher() + } +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/watcher/view/impl/logging/LoggingWebAssertionWatcherInterceptor.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/watcher/view/impl/logging/LoggingWebAssertionWatcherInterceptor.kt index 94150bb62..4f852dbdc 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/watcher/view/impl/logging/LoggingWebAssertionWatcherInterceptor.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/watcher/view/impl/logging/LoggingWebAssertionWatcherInterceptor.kt @@ -16,7 +16,7 @@ class LoggingWebAssertionWatcherInterceptor( ) : WebAssertionWatcherInterceptor { /** - * Writes info to [compositeLogger]. + * Writes info to [UiTestLogger]. * * @param webAssertionProxy a proxy-wrapper of [androidx.test.espresso.web.assertion.WebAssertion] for * interceptors calls. @@ -25,20 +25,20 @@ class LoggingWebAssertionWatcherInterceptor( * @param result a result of [androidx.test.espresso.web.assertion.WebAssertion]. */ override fun intercept(webAssertionProxy: WebAssertionProxy<*>, view: WebView?, result: Any) { - logger.i(getFullWebAssertionDescription(webAssertionProxy, result)) + logger.i(webAssertionProxy.fullDescription(result)) } /** - * @return a string description of [WebAssertion]. + * @return a string description of [WebAssertionProxy]. */ - private fun getFullWebAssertionDescription(webAssertionProxy: WebAssertionProxy<*>, result: Any): String { + private fun WebAssertionProxy<*>.fullDescription(result: Any): String { return StringBuilder("web assertion") .apply { - webAssertionProxy.webAssertion.describeTo(this, result) + webAssertion.describeTo(this, result) } .apply { append(" on webview ") - webAssertionProxy.matcher.describeTo(StringDescription(this)) + matcher.describeTo(StringDescription(this)) } .toString() } diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/exceptions/ThrowableWithInteraction.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/exceptions/ThrowableWithInteraction.kt new file mode 100644 index 000000000..ca37d83a1 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/exceptions/ThrowableWithInteraction.kt @@ -0,0 +1,6 @@ +package com.kaspersky.kaspresso.internal.exceptions + +class ThrowableWithInteraction( + val throwable: Throwable, + val interaction: Any +) : Throwable(null, throwable.cause) \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/extensions/espressoext/MatchersExt.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/extensions/espressoext/MatchersExt.kt index 9413de888..c77254067 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/extensions/espressoext/MatchersExt.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/extensions/espressoext/MatchersExt.kt @@ -7,7 +7,7 @@ import org.hamcrest.StringDescription /** * @return a [String] description of [Matcher]. */ -internal fun Matcher?.describe(): String { +internal fun Matcher?.describe(): String { if (this == null) return "null" val builder = StringBuilder() diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt index 51b9b89bc..a70ce99ea 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt @@ -34,7 +34,9 @@ import com.kaspersky.kaspresso.device.screenshots.Screenshots import com.kaspersky.kaspresso.device.screenshots.ScreenshotsImpl import com.kaspersky.kaspresso.device.server.AdbServer import com.kaspersky.kaspresso.device.server.AdbServerImpl +import com.kaspersky.kaspresso.failure.FailureLoggingParams import com.kaspersky.kaspresso.failure.LoggingFailureHandler +import com.kaspersky.kaspresso.failure.describe.AssertionCache import com.kaspersky.kaspresso.flakysafety.FlakySafetyParams import com.kaspersky.kaspresso.interceptors.behavior.DataBehaviorInterceptor import com.kaspersky.kaspresso.interceptors.behavior.ViewBehaviorInterceptor @@ -64,12 +66,15 @@ import com.kaspersky.kaspresso.interceptors.watcher.view.AtomWatcherInterceptor import com.kaspersky.kaspresso.interceptors.watcher.view.ViewActionWatcherInterceptor import com.kaspersky.kaspresso.interceptors.watcher.view.ViewAssertionWatcherInterceptor import com.kaspersky.kaspresso.interceptors.watcher.view.WebAssertionWatcherInterceptor +import com.kaspersky.kaspresso.interceptors.watcher.view.impl.caching.CachingViewAssertionWatcherInterceptor import com.kaspersky.kaspresso.interceptors.watcher.view.impl.logging.LoggingAtomWatcherInterceptor import com.kaspersky.kaspresso.interceptors.watcher.view.impl.logging.LoggingViewActionWatcherInterceptor import com.kaspersky.kaspresso.interceptors.watcher.view.impl.logging.LoggingViewAssertionWatcherInterceptor import com.kaspersky.kaspresso.interceptors.watcher.view.impl.logging.LoggingWebAssertionWatcherInterceptor import com.kaspersky.kaspresso.logger.UiTestLogger import com.kaspersky.kaspresso.logger.UiTestLoggerImpl +import com.kaspersky.kaspresso.failure.describe.FailedViewAssertionDescriber +import com.kaspersky.kaspresso.failure.describe.FailedWebAssertionDescriber import com.kaspersky.kaspresso.report.impl.AllureReportWriter /** @@ -82,6 +87,7 @@ data class Kaspresso( internal val device: Device, internal val flakySafetyParams: FlakySafetyParams, internal val autoScrollParams: AutoScrollParams, + internal val failureLoggingParams: FailureLoggingParams, internal val viewActionWatcherInterceptors: List, internal val viewAssertionWatcherInterceptors: List, internal val atomWatcherInterceptors: List, @@ -113,13 +119,15 @@ data class Kaspresso( */ fun default(): Builder { return Builder().apply { + val assertionCache = AssertionCache() viewActionWatcherInterceptors = mutableListOf( LoggingViewActionWatcherInterceptor(libLogger) ) viewAssertionWatcherInterceptors = mutableListOf( - LoggingViewAssertionWatcherInterceptor(libLogger) + LoggingViewAssertionWatcherInterceptor(libLogger), + CachingViewAssertionWatcherInterceptor(assertionCache) ) atomWatcherInterceptors = mutableListOf( @@ -134,7 +142,13 @@ data class Kaspresso( AutoScrollViewBehaviorInterceptor(autoScrollParams, libLogger), SystemDialogSafetyViewBehaviorInterceptor(libLogger, uiDevice), FlakySafeViewBehaviorInterceptor(flakySafetyParams, libLogger), - FailureLoggingViewBehaviorInterceptor(libLogger) + FailureLoggingViewBehaviorInterceptor( + libLogger, + FailedViewAssertionDescriber( + failureLoggingParams, + assertionCache + ) + ) ) dataBehaviorInterceptors = mutableListOf( @@ -147,7 +161,13 @@ data class Kaspresso( AutoScrollWebBehaviorInterceptor(autoScrollParams, libLogger), SystemDialogSafetyWebBehaviorInterceptor(libLogger, uiDevice), FlakySafeWebBehaviorInterceptor(flakySafetyParams, libLogger), - FailureLoggingWebBehaviorInterceptor(libLogger) + FailureLoggingWebBehaviorInterceptor( + libLogger, + FailedWebAssertionDescriber( + failureLoggingParams, + assertionCache + ) + ) ) stepWatcherInterceptors = mutableListOf( @@ -258,6 +278,12 @@ data class Kaspresso( */ var autoScrollParams: AutoScrollParams = AutoScrollParams() + /** + * Holds the [FailureLoggingParams] for [com.kaspersky.kaspresso.failure.FailureLoggingProvider]'s usage. + * If it was not specified, the default implementation is used. + */ + var failureLoggingParams: FailureLoggingParams = FailureLoggingParams() + /** * Holds the list of [ViewActionWatcherInterceptor]s. * If it was not specified, Kaspresso will use no [ViewActionWatcherInterceptor]s. @@ -407,6 +433,7 @@ data class Kaspresso( flakySafetyParams = flakySafetyParams, autoScrollParams = autoScrollParams, + failureLoggingParams = failureLoggingParams, viewActionWatcherInterceptors = viewActionWatcherInterceptors, viewAssertionWatcherInterceptors = viewAssertionWatcherInterceptors, diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainTextViewAction.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainTextViewAction.kt new file mode 100644 index 000000000..146b6384d --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainTextViewAction.kt @@ -0,0 +1,19 @@ +package com.kaspersky.kaspresso.obtain_value.actions + +import android.view.View +import android.widget.TextView +import androidx.test.espresso.UiController +import androidx.test.espresso.matcher.ViewMatchers +import org.hamcrest.Matcher + +class ObtainTextViewAction : ObtainValueViewAction { + override var value: String = "with text \"" + + override fun getConstraints(): Matcher = ViewMatchers.isAssignableFrom(TextView::class.java) + + override fun getDescription(): String = "getting text from a TextView" + + override fun perform(uiController: UiController?, view: View?) { + value += "${(view as TextView).text}\"" + } +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainValueViewAction.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainValueViewAction.kt new file mode 100644 index 000000000..4baef5ef2 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainValueViewAction.kt @@ -0,0 +1,7 @@ +package com.kaspersky.kaspresso.obtain_value.actions + +import androidx.test.espresso.ViewAction + +interface ObtainValueViewAction : ViewAction { + var value: String +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainVisibilityViewAction.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainVisibilityViewAction.kt new file mode 100644 index 000000000..8b8ef0d0c --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/actions/ObtainVisibilityViewAction.kt @@ -0,0 +1,23 @@ +package com.kaspersky.kaspresso.obtain_value.actions + +import android.view.View +import androidx.test.espresso.UiController +import androidx.test.espresso.matcher.ViewMatchers +import org.hamcrest.Matcher + +class ObtainVisibilityViewAction : ObtainValueViewAction { + override var value: String = "with visibility \"" + + override fun getConstraints(): Matcher = ViewMatchers.isAssignableFrom(View::class.java) + + override fun getDescription(): String = "getting visibility from a View" + + override fun perform(uiController: UiController?, view: View?) { + value += when (view?.visibility) { + View.VISIBLE -> "VISIBLE" + View.INVISIBLE -> "INVISIBLE" + View.GONE -> "GONE" + else -> "[UNDEFINED]" + } + } +} \ No newline at end of file diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/atom/ObtainValueAtom.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/atom/ObtainValueAtom.kt new file mode 100644 index 000000000..f2e29b2ad --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/obtain_value/atom/ObtainValueAtom.kt @@ -0,0 +1,5 @@ +package com.kaspersky.kaspresso.obtain_value.atom + +import androidx.test.espresso.web.model.Atom + +interface ObtainValueAtom : Atom \ No newline at end of file