Search code examples
androidimageviewandroid-imageviewandroid-espressoandroid-testing

Comparing two VectorDrawables fails in Kaspresso - Android


I'm trying to use Kaspresso for tests and I'm checking whether a view has a certain drawable with the method:

withId<KImageView>(R.id.criticalErrorImage) {
    hasDrawable(R.drawable.error_graphic)
}

With PNG works really well, while comparing the image stored in the imageView and restored and the VectorDrawable fails.

The file where the check is made is this one.

In particular this part of code:

            var expectedDrawable: Drawable? = drawable ?: getResourceDrawable(resId)?.mutate()

            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && expectedDrawable != null) {
                expectedDrawable = DrawableCompat.wrap(expectedDrawable).mutate()
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                tintColorId?.let { tintColorId ->
                    val tintColor = getResourceColor(tintColorId)
                    expectedDrawable?.apply {
                        setTintList(ColorStateList.valueOf(tintColor))
                        setTintMode(PorterDuff.Mode.SRC_IN)
                    }
                }
            }

            if (expectedDrawable == null) {
                return false
            }

            val convertDrawable = (imageView as ImageView).drawable.mutate()
            val bitmap = toBitmap?.invoke(convertDrawable) ?: convertDrawable.toBitmap()

            val otherBitmap = toBitmap?.invoke(expectedDrawable) ?: expectedDrawable.toBitmap()

The funny part is that it works if I'm setting the image through databinding, while it doesn't work if I set it in all other ways. Cannot understand why.

I've created a POC here: https://github.com/filnik/proof_of_concept

In particular, here is the test: https://github.com/filnik/proof_of_concept/blob/master/app/src/androidTest/java/com/neato/test/POCInstrumentationTest.kt

While here I dynamically set the image: https://github.com/filnik/proof_of_concept/blob/master/app/src/main/java/com/neato/test/FirstFragment.kt


Solution

  • The issue was because actually the image is scaled. So the scaled image is different from the original one.

    To avoid this issue I've used this "altered" KImageView:

    package your_package
    
    import android.content.res.ColorStateList
    import android.graphics.Bitmap
    import android.graphics.Canvas
    import android.graphics.PorterDuff
    import android.graphics.drawable.BitmapDrawable
    import android.graphics.drawable.Drawable
    import android.graphics.drawable.StateListDrawable
    import android.os.Build
    import android.view.View
    import android.widget.ImageView
    import androidx.annotation.ColorRes
    import androidx.annotation.DrawableRes
    import androidx.core.graphics.drawable.DrawableCompat
    import androidx.test.espresso.DataInteraction
    import androidx.test.espresso.assertion.ViewAssertions
    import com.agoda.kakao.common.assertions.BaseAssertions
    import com.agoda.kakao.common.builders.ViewBuilder
    import com.agoda.kakao.common.utilities.getResourceColor
    import com.agoda.kakao.common.utilities.getResourceDrawable
    import com.agoda.kakao.common.views.KBaseView
    import com.agoda.kakao.image.KImageView
    import org.hamcrest.Description
    import org.hamcrest.Matcher
    import org.hamcrest.TypeSafeMatcher
    
    class KImageView2 : KBaseView<KImageView>, ImageViewAssertions2 {
        constructor(function: ViewBuilder.() -> Unit) : super(function)
        constructor(parent: Matcher<View>, function: ViewBuilder.() -> Unit) : super(parent, function)
        constructor(parent: DataInteraction, function: ViewBuilder.() -> Unit) : super(parent, function)
    }
    
    
    interface ImageViewAssertions2 : BaseAssertions {
        /**
         * Checks if the view displays given drawable
         *
         * @param resId Drawable resource to be matched
         * @param toBitmap Lambda with custom Drawable -> Bitmap converter (default is null)
         */
        fun hasDrawable(@DrawableRes resId: Int, toBitmap: ((drawable: Drawable) -> Bitmap)? = null) {
            view.check(ViewAssertions.matches(DrawableMatcher2(resId = resId, toBitmap = toBitmap)))
        }
    }
    
    
    class DrawableMatcher2(
        @DrawableRes private val resId: Int = -1,
        @ColorRes private val tintColorId: Int? = null,
        private val toBitmap: ((drawable: Drawable) -> Bitmap)? = null
    ) : TypeSafeMatcher<View>(View::class.java) {
    
        override fun describeTo(desc: Description) {
            desc.appendText("with drawable id $resId or provided instance")
        }
    
        override fun matchesSafely(view: View?): Boolean {
            if (view !is ImageView) {
                return false
            }
    
            if (resId < 0) {
                return view.drawable == null
            }
            val bitmap = extractFromImageView(view)
            view.setImageResource(resId)
            val otherBitmap = extractFromImageView(view)
    
            return bitmap?.sameAs(otherBitmap) ?: false
        }
    
        private fun extractFromImageView(view: View?): Bitmap? {
            return view?.let { imageView ->
                var expectedDrawable: Drawable? = getResourceDrawable(resId)?.mutate()
    
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && expectedDrawable != null) {
                    expectedDrawable = DrawableCompat.wrap(expectedDrawable).mutate()
                }
    
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    tintColorId?.let { tintColorId ->
                        val tintColor = getResourceColor(tintColorId)
                        expectedDrawable?.apply {
                            setTintList(ColorStateList.valueOf(tintColor))
                            setTintMode(PorterDuff.Mode.SRC_IN)
                        }
                    }
                }
    
                if (expectedDrawable == null) {
                    return null
                }
    
                val convertDrawable = (imageView as ImageView).drawable.mutate()
                toBitmap?.invoke(convertDrawable) ?: convertDrawable.toBitmap()
            }
        }
    }
    
    internal fun Drawable.toBitmap(): Bitmap {
        if (this is BitmapDrawable && this.bitmap != null) {
            return this.bitmap
        }
    
        if (this is StateListDrawable && this.getCurrent() is BitmapDrawable) {
            val bitmapDrawable = this.getCurrent() as BitmapDrawable
            if (bitmapDrawable.bitmap != null) {
                return bitmapDrawable.bitmap
            }
        }
    
        val bitmap = if (this.intrinsicWidth <= 0 || this.intrinsicHeight <= 0) {
            Bitmap.createBitmap(
                1,
                1,
                Bitmap.Config.ARGB_8888
            ) // Single color bitmap will be created of 1x1 pixel
        } else {
            Bitmap.createBitmap(this.intrinsicWidth, this.intrinsicHeight, Bitmap.Config.ARGB_8888)
        }
    
        val canvas = Canvas(bitmap)
        this.setBounds(0, 0, canvas.width, canvas.height)
        this.draw(canvas)
        return bitmap
    }
    

    Not the cleanest solution possible, but it works. If anyone can provide a better solution would be great :-)