diff --git a/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/ChipAssertions.kt b/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/ChipAssertions.kt new file mode 100644 index 000000000..95e413759 --- /dev/null +++ b/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/ChipAssertions.kt @@ -0,0 +1,83 @@ +package io.github.kakaocup.kakao.chip + +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.test.espresso.assertion.ViewAssertions +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipDrawable +import io.github.kakaocup.kakao.common.assertions.BaseAssertions +import org.hamcrest.Description +import org.hamcrest.TypeSafeMatcher + +interface ChipAssertions : BaseAssertions { + + /** + * Check if Chip has correct icon for [ChipIconType.CHECKED] icon type. + * + * @param resId Drawable resource to be matched (default is -1) + * @param drawable Drawable instance to be matched (default is null) + * @param tintColorId Tint color resource id (default is null) + * @param toBitmap Lambda with custom Drawable -> Bitmap converter (default is null) + */ + fun hasCheckedIcon( + @DrawableRes resId: Int = -1, + drawable: Drawable? = null, + @ColorRes tintColorId: Int? = null, + toBitmap: ((drawable: Drawable) -> Bitmap)? = null, + ) = view.check(ViewAssertions.matches(ChipDrawableMatcher(resId, drawable, ChipIconType.CHECKED, tintColorId, toBitmap))) + + + /** + * Check if Chip has correct icon for [ChipIconType.CHIP] icon type. + * + * @param resId Drawable resource to be matched (default is -1) + * @param drawable Drawable instance to be matched (default is null) + * @param tintColorId Tint color resource id (default is null) + * @param toBitmap Lambda with custom Drawable -> Bitmap converter (default is null) + */ + fun hasChipIcon( + @DrawableRes resId: Int = -1, + drawable: Drawable? = null, + @ColorRes tintColorId: Int? = null, + toBitmap: ((drawable: Drawable) -> Bitmap)? = null, + ) = view.check(ViewAssertions.matches(ChipDrawableMatcher(resId, drawable, ChipIconType.CHIP, tintColorId, toBitmap))) + + /** + * Check if Chip has correct icon for [ChipIconType.CLOSE] icon type. + * + * @param resId Drawable resource to be matched (default is -1) + * @param drawable Drawable instance to be matched (default is null) + * @param tintColorId Tint color resource id (default is null) + * @param toBitmap Lambda with custom Drawable -> Bitmap converter (default is null) + */ + fun hasCloseIcon( + @DrawableRes resId: Int = -1, + drawable: Drawable? = null, + @ColorRes tintColorId: Int? = null, + toBitmap: ((drawable: Drawable) -> Bitmap)? = null, + ) = view.check(ViewAssertions.matches(ChipDrawableMatcher(resId, drawable, ChipIconType.CLOSE, tintColorId, toBitmap))) + + /** + * Verify all icons are hidden + */ + fun hasNoIconVisible() = view.check(ViewAssertions.matches(object : TypeSafeMatcher(View::class.java) { + override fun describeTo(desc: Description) { + desc.appendText("without any icons visible") + } + + override fun matchesSafely(view: View): Boolean { + val viewAsChip = view as? Chip ?: return false + return (viewAsChip.chipDrawable as? ChipDrawable)?.verifyNoIconVisible() ?: run { + println("Drawable should be ChipDrawable unless implementation has changed") + false + } + } + })) + + fun ChipDrawable.verifyNoIconVisible(): Boolean { + return !isCheckedIconVisible && !isChipIconVisible && !isCloseIconVisible + } +} diff --git a/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/ChipDrawableMatcher.kt b/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/ChipDrawableMatcher.kt new file mode 100644 index 000000000..5f17a9418 --- /dev/null +++ b/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/ChipDrawableMatcher.kt @@ -0,0 +1,85 @@ +package io.github.kakaocup.kakao.chip + +import android.content.res.ColorStateList +import android.graphics.Bitmap +import android.graphics.PorterDuff +import android.graphics.drawable.Drawable +import android.os.Build +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.graphics.drawable.DrawableCompat +import com.google.android.material.chip.Chip +import io.github.kakaocup.kakao.common.extentions.toBitmap +import io.github.kakaocup.kakao.common.utilities.getResourceColor +import io.github.kakaocup.kakao.common.utilities.getResourceDrawable +import org.hamcrest.Description +import org.hamcrest.TypeSafeMatcher + +/** + * Matches given drawable with current one for the [chipIconType] drawable type. + * + * @param resId Drawable resource to be matched (default is -1) + * @param drawable Drawable instance to be matched (default is null) + * @param chipIconType drawable type to verify against + * @param tintColorId Tint color resource id (default is null) + * @param toBitmap Lambda with custom Drawable -> Bitmap converter (default is null) + */ +class ChipDrawableMatcher( + @DrawableRes private val resId: Int = -1, + private val drawable: Drawable? = null, + private val chipIconType: ChipIconType, + @ColorRes private val tintColorId: Int? = null, + private val toBitmap: ((drawable: Drawable) -> Bitmap)? = null, +) : TypeSafeMatcher(View::class.java) { + + override fun describeTo(desc: Description) { + desc.appendText("with drawable id $resId or provided instance for the $chipIconType icon") + } + + @Suppress("ReturnCount") + override fun matchesSafely(view: View?): Boolean { + val expectedDrawable: Drawable? = when { + drawable != null -> drawable + resId >= 0 -> getResourceDrawable(resId)?.mutate() + else -> return (view as? Chip)?.chipDrawable == null + } + + // Apply backward compatibility wrap and tints if necessary + val finalDrawable = expectedDrawable.processDrawableForDueToSdk() ?: return false + + // Compare actual vs expected drawables + return (view as? Chip)?.getChipActualDrawable(chipIconType)?.mutate()?.let { actualDrawable -> + val actualBitmap = toBitmap?.invoke(actualDrawable) ?: actualDrawable.toBitmap() + val expectedBitmap = toBitmap?.invoke(finalDrawable) ?: finalDrawable.toBitmap() + actualBitmap.sameAs(expectedBitmap) + } ?: false + } + + private fun Drawable?.processDrawableForDueToSdk() = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && this != null -> DrawableCompat.wrap( + this + ) + .mutate() + + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && tintColorId != null -> this?.apply { + val tintColor = getResourceColor(tintColorId) + setTintList(ColorStateList.valueOf(tintColor)) + setTintMode(PorterDuff.Mode.SRC_IN) + } + + else -> this + } + + private fun Chip.getChipActualDrawable(chipIconType: ChipIconType): Drawable? = when (chipIconType) { + ChipIconType.CHECKED -> checkedIcon?.takeIf { isCheckedIconVisible } + ChipIconType.CHIP -> chipIcon?.takeIf { isChipIconVisible } + ChipIconType.CLOSE -> closeIcon?.takeIf { isCloseIconVisible } + } +} + +enum class ChipIconType { + CHECKED, + CHIP, + CLOSE, +} diff --git a/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/KChip.kt b/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/KChip.kt new file mode 100644 index 000000000..e15160030 --- /dev/null +++ b/kakao/src/main/kotlin/io/github/kakaocup/kakao/chip/KChip.kt @@ -0,0 +1,16 @@ +package io.github.kakaocup.kakao.chip + +import android.view.View +import androidx.test.espresso.DataInteraction +import io.github.kakaocup.kakao.check.CheckableActions +import io.github.kakaocup.kakao.check.CheckableAssertions +import io.github.kakaocup.kakao.common.builders.ViewBuilder +import io.github.kakaocup.kakao.common.views.KBaseView +import io.github.kakaocup.kakao.text.TextViewAssertions +import org.hamcrest.Matcher + +class KChip : KBaseView, CheckableActions, CheckableAssertions, TextViewAssertions, ChipAssertions { + constructor(function: ViewBuilder.() -> Unit) : super(function) + constructor(parent: Matcher, function: ViewBuilder.() -> Unit) : super(parent, function) + constructor(parent: DataInteraction, function: ViewBuilder.() -> Unit) : super(parent, function) +} diff --git a/sample/src/androidTest/kotlin/io/github/kakaocup/sample/ChipsTest.kt b/sample/src/androidTest/kotlin/io/github/kakaocup/sample/ChipsTest.kt new file mode 100644 index 000000000..fb92595c1 --- /dev/null +++ b/sample/src/androidTest/kotlin/io/github/kakaocup/sample/ChipsTest.kt @@ -0,0 +1,42 @@ +package io.github.kakaocup.sample + +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import io.github.kakaocup.kakao.screen.Screen.Companion.onScreen +import io.github.kakaocup.sample.screen.ChipsScreen +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4ClassRunner::class) +class ChipsTest { + @Rule + @JvmField + val rule = ActivityScenarioRule(ChipsActivity::class.java) + + @Test + fun testCorrectChipsDisplayed() { + onScreen { + chip1 { + isChecked() + hasText("Chip1") + hasCheckedIcon(R.drawable.ic_sentiment_very_satisfied_black_24dp) + } + chip2 { + isNotChecked() + hasText("Chip2") + hasChipIcon(R.drawable.ic_auto_fix_high_24dp) + } + chip3 { + isNotChecked() + hasText("Chip3") + hasCloseIcon(R.drawable.ic_android_black_24dp, tintColorId = android.R.color.black) + } + chip4 { + isNotChecked() + hasText("Chip4") + hasNoIconVisible() + } + } + } +} diff --git a/sample/src/androidTest/kotlin/io/github/kakaocup/sample/screen/ChipsScreen.kt b/sample/src/androidTest/kotlin/io/github/kakaocup/sample/screen/ChipsScreen.kt new file mode 100644 index 000000000..970efee0e --- /dev/null +++ b/sample/src/androidTest/kotlin/io/github/kakaocup/sample/screen/ChipsScreen.kt @@ -0,0 +1,12 @@ +package io.github.kakaocup.sample.screen + +import io.github.kakaocup.kakao.chip.KChip +import io.github.kakaocup.kakao.screen.Screen +import io.github.kakaocup.sample.R + +open class ChipsScreen : Screen() { + val chip1: KChip = KChip { withId(R.id.chip1) } + val chip2: KChip = KChip { withId(R.id.chip2) } + val chip3: KChip = KChip { withId(R.id.chip3) } + val chip4: KChip = KChip { withId(R.id.chip4) } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 4f8ae549d..036bac892 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -128,6 +128,10 @@ android:name="io.github.kakaocup.sample.AnimatedButtonClickActivity" android:label="Animated Button Clicks Activity" android:theme="@style/MaterialAppTheme" /> + diff --git a/sample/src/main/kotlin/io/github/kakaocup/sample/ChipsActivity.kt b/sample/src/main/kotlin/io/github/kakaocup/sample/ChipsActivity.kt new file mode 100644 index 000000000..cc5217bb8 --- /dev/null +++ b/sample/src/main/kotlin/io/github/kakaocup/sample/ChipsActivity.kt @@ -0,0 +1,11 @@ +package io.github.kakaocup.sample + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class ChipsActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_chips) + } +} diff --git a/sample/src/main/kotlin/io/github/kakaocup/sample/TestActivity.kt b/sample/src/main/kotlin/io/github/kakaocup/sample/TestActivity.kt index ab9be766d..06accead9 100644 --- a/sample/src/main/kotlin/io/github/kakaocup/sample/TestActivity.kt +++ b/sample/src/main/kotlin/io/github/kakaocup/sample/TestActivity.kt @@ -41,6 +41,7 @@ class TestActivity : AppCompatActivity() { addRoute(R.id.text_input_layout, TextInputLayoutActivity::class.java) addRoute(R.id.text_activity, TextActivity::class.java) addRoute(R.id.switchers_button, SwitchersActivity::class.java) + addRoute(R.id.chips, ChipsActivity::class.java) findViewById