From 7eb162b0d32d07aca6f8ad618212903ab61d316d Mon Sep 17 00:00:00 2001 From: Evgeny Meltsaykin Date: Thu, 7 Dec 2023 00:30:11 +0500 Subject: [PATCH] Enhancement - Add support for content checking and tint color for compose icon --- .../node/assertion/ImageContentAssertions.kt | 41 +++++++++++ .../node/assertion/TintColorAssertions.kt | 70 +++++++++++++++++++ .../compose/node/element/KIconNode.kt | 18 +++++ .../kakaocup/compose/utilities/ColorUtils.kt | 8 +++ .../kakaocup/compose/node/KAppIconNode.kt | 24 +++++++ .../compose/screen/MainActivityScreen.kt | 13 ++++ .../kakaocup/compose/test/SimpleTest.kt | 53 +++++++++++++- .../kakaocup/compose/sample/MainScreen.kt | 56 ++++++++++++++- .../compose/sample/semantics/Color.kt | 13 ++++ .../compose/sample/semantics/Image.kt | 12 ++++ sample/src/main/res/drawable/ic_adb.xml | 5 ++ sample/src/main/res/drawable/ic_android.xml | 5 ++ sample/src/main/res/values/strings.xml | 1 + 13 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ImageContentAssertions.kt create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TintColorAssertions.kt create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KIconNode.kt create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ColorUtils.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppIconNode.kt create mode 100644 sample/src/main/java/io/github/kakaocup/compose/sample/semantics/Color.kt create mode 100644 sample/src/main/java/io/github/kakaocup/compose/sample/semantics/Image.kt create mode 100644 sample/src/main/res/drawable/ic_adb.xml create mode 100644 sample/src/main/res/drawable/ic_android.xml diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ImageContentAssertions.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ImageContentAssertions.kt new file mode 100644 index 00000000..ab52347a --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ImageContentAssertions.kt @@ -0,0 +1,41 @@ +package io.github.kakaocup.compose.node.assertion + +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert + +interface ImageContentAssertions : NodeAssertions { + val imageContentSemanticsPropertyKey: SemanticsPropertyKey + + /** + * Asserts that the image or icon content contains the given [imageVector]. + * + * Throws [AssertionError] if the image or icon content value is not equal to `imageVector`. + * Throws [IllegalStateException] if the image or icon does not contain the [imageContentSemanticsPropertyKey] modifier. + */ + fun assertContentEquals(imageVector: ImageVector) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { assert(hasImageContent(imageVector)) } + } + + /** + * Asserts that the image or icon content contains the given [drawableRes]. + * + * Throws [AssertionError] if the image or icon content value is not equal to `drawableRes`. + * Throws [IllegalStateException] if the image or icon does not contain the [imageContentSemanticsPropertyKey] modifier. + */ + fun assertContentEquals(@DrawableRes drawableRes: Int) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { assert(hasImageContent(drawableRes)) } + } + + private fun hasImageContent(expectedContent: Any): SemanticsMatcher = SemanticsMatcher( + "The content is expected to be $expectedContent, but the actual content is different" + ) { node -> + val actual = node.config.getOrNull(imageContentSemanticsPropertyKey) + ?: error("Compose view does not contain $imageContentSemanticsPropertyKey modifier") + + return@SemanticsMatcher actual == expectedContent + } +} \ No newline at end of file diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TintColorAssertions.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TintColorAssertions.kt new file mode 100644 index 00000000..0116b77d --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TintColorAssertions.kt @@ -0,0 +1,70 @@ +package io.github.kakaocup.compose.node.assertion + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert +import io.github.kakaocup.compose.utilities.getComposeColor + +interface TintColorAssertions : NodeAssertions { + val tintColorSemanticsPropertyKey: SemanticsPropertyKey + + /** + * Asserts that the compose view tint color contains the given [color]. + * + * Throws [AssertionError] if the compose view tint color value is not equal to `color`. + * Throws [IllegalStateException] if the compose view does not contain the [tintColorSemanticsPropertyKey] modifier. + */ + fun assertTintColorEquals(color: Color) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { assert(hasColor(color)) } + } + + /** + * Asserts that the compose view tint color contains the given [color]. + * + * Throws [AssertionError] if the compose view tint color value is not equal to `color`. + * Throws [IllegalStateException] if the compose view does not contain the [tintColorSemanticsPropertyKey] modifier. + * Throws [IllegalArgumentException] if the color value is incorrect. + */ + fun assertTintColorEquals(color: String) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { assert(hasColor(color)) } + } + + /** + * Asserts that the compose view tint color contains the given [color]. + * + * Throws [AssertionError] if the compose view tint color value is not equal to `color`. + * Throws [IllegalStateException] if the compose view does not contain the [tintColorSemanticsPropertyKey] modifier. + */ + fun assertTintColorEquals(color: Long) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { assert(hasColor(color)) } + } + + private fun hasColor(expectedColor: Color): SemanticsMatcher = SemanticsMatcher( + "The color is expected to be $expectedColor, but the actual color is different" + ) { node -> + val actualColor = node.config.getOrNull(tintColorSemanticsPropertyKey) + ?: error("Compose view does not contain $tintColorSemanticsPropertyKey modifier") + + return@SemanticsMatcher actualColor == expectedColor + } + + private fun hasColor(expectedColor: String): SemanticsMatcher = SemanticsMatcher( + "The color is expected to be $expectedColor, but the actual color is different" + ) { node -> + val actualColor = node.config.getOrNull(tintColorSemanticsPropertyKey) + ?: error("Compose view does not contain $tintColorSemanticsPropertyKey modifier") + + return@SemanticsMatcher actualColor == getComposeColor(expectedColor) + } + + private fun hasColor(expectedColor: Long): SemanticsMatcher = SemanticsMatcher( + "The color is expected to be $expectedColor, but the actual color is different" + ) { node -> + val actualColor = node.config.getOrNull(tintColorSemanticsPropertyKey) + ?: error("Compose view does not contain $tintColorSemanticsPropertyKey modifier") + + return@SemanticsMatcher actualColor == Color(expectedColor) + } +} \ No newline at end of file diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KIconNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KIconNode.kt new file mode 100644 index 00000000..9d44e84f --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KIconNode.kt @@ -0,0 +1,18 @@ +package io.github.kakaocup.compose.node.element + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.node.builder.NodeMatcher +import io.github.kakaocup.compose.node.core.BaseNode +import io.github.kakaocup.compose.node.assertion.ImageContentAssertions +import io.github.kakaocup.compose.node.assertion.TintColorAssertions + +abstract class KIconNode( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>?, + useUnmergedTree: Boolean = false +) : BaseNode( + semanticsProvider = semanticsProvider, + nodeMatcher = nodeMatcher.copy(useUnmergedTree = useUnmergedTree), + parentNode = parentNode +), ImageContentAssertions, TintColorAssertions \ No newline at end of file diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ColorUtils.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ColorUtils.kt new file mode 100644 index 00000000..f000d740 --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ColorUtils.kt @@ -0,0 +1,8 @@ +package io.github.kakaocup.compose.utilities + +import androidx.compose.ui.graphics.Color + +internal fun getComposeColor(color: String): Color { + val normalizeColor = if (color.contains("#")) color else "#$color" + return Color(android.graphics.Color.parseColor(normalizeColor)) +} \ No newline at end of file diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppIconNode.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppIconNode.kt new file mode 100644 index 00000000..b66b027a --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppIconNode.kt @@ -0,0 +1,24 @@ +package io.github.kakaocup.compose.node + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.node.builder.NodeMatcher +import io.github.kakaocup.compose.node.core.BaseNode +import io.github.kakaocup.compose.node.element.KIconNode +import io.github.kakaocup.compose.sample.semantics.ImageContentSemanticKey +import io.github.kakaocup.compose.sample.semantics.TintColorSemanticKey + +class KAppIconNode( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>? = null, +) : KIconNode( + semanticsProvider = semanticsProvider, + nodeMatcher = nodeMatcher, + parentNode = parentNode, + useUnmergedTree = true +) { + override val imageContentSemanticsPropertyKey: SemanticsPropertyKey = ImageContentSemanticKey + override val tintColorSemanticsPropertyKey: SemanticsPropertyKey = TintColorSemanticKey +} \ No newline at end of file diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/screen/MainActivityScreen.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/MainActivityScreen.kt index d58ad7ec..4a41ebb8 100644 --- a/sample/src/androidTest/java/io/github/kakaocup/compose/screen/MainActivityScreen.kt +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/MainActivityScreen.kt @@ -1,6 +1,7 @@ package io.github.kakaocup.compose.screen import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.node.KAppIconNode import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.KNode @@ -20,6 +21,18 @@ class MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : hasPosition(1) } + val changeIconsButton: KNode = child { + hasTestTag("changeIconButton") + } + + val iconDrawableRes: KAppIconNode = child { + hasTestTag("iconDrawableRes") + } + + val iconImageVector: KAppIconNode = child { + hasTestTag("iconImageVector") + } + val myButton: KNode = child { hasTestTag("myTestButton") hasText("Button 1") diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTest.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTest.kt index 86f29d0f..eb644382 100644 --- a/sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTest.kt +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTest.kt @@ -1,8 +1,13 @@ package io.github.kakaocup.compose.test +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Call +import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.junit4.createAndroidComposeRule import io.github.kakaocup.compose.sample.MainActivity import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.sample.R import io.github.kakaocup.compose.screen.MainActivityScreen import org.junit.Rule import org.junit.Test @@ -18,7 +23,7 @@ class SimpleTest { myButton { assertIsDisplayed() assertTextContains("Button 1") - assertTextContains(io.github.kakaocup.compose.sample.R.string.button_1) + assertTextContains(R.string.button_1) } myText1 { @@ -38,4 +43,50 @@ class SimpleTest { } } } + + @Test + fun iconTest() { + onComposeScreen(composeTestRule) { + changeIconsButton{ + assertIsDisplayed() + } + iconDrawableRes{ + assertIsDisplayed() + assertContentEquals(R.drawable.ic_android) + assertTintColorEquals(Color.Black) + assertTintColorEquals("000000") + assertTintColorEquals("#000000") + assertTintColorEquals(0xFF000000) + } + + iconImageVector{ + assertIsDisplayed() + assertContentEquals(Icons.Filled.AccountCircle) + assertTintColorEquals(Color.Black) + assertTintColorEquals("000000") + assertTintColorEquals("#000000") + assertTintColorEquals(0xFF000000) + } + + changeIconsButton.performClick() + + iconDrawableRes{ + assertIsDisplayed() + assertContentEquals(R.drawable.ic_adb) + assertTintColorEquals(Color.Blue) + assertTintColorEquals("0000FF") + assertTintColorEquals("#0000FF") + assertTintColorEquals(0xFF0000FF) + } + + iconImageVector{ + assertIsDisplayed() + assertContentEquals(Icons.Filled.Call) + assertTintColorEquals(Color.Blue) + assertTintColorEquals("0000FF") + assertTintColorEquals("#0000FF") + assertTintColorEquals(0xFF0000FF) + } + } + } } \ No newline at end of file diff --git a/sample/src/main/java/io/github/kakaocup/compose/sample/MainScreen.kt b/sample/src/main/java/io/github/kakaocup/compose/sample/MainScreen.kt index f8356ab0..4ae630a6 100644 --- a/sample/src/main/java/io/github/kakaocup/compose/sample/MainScreen.kt +++ b/sample/src/main/java/io/github/kakaocup/compose/sample/MainScreen.kt @@ -1,25 +1,40 @@ package io.github.kakaocup.compose.sample import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Button +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Call import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource +import io.github.kakaocup.compose.sample.semantics.imageContentSemantic +import io.github.kakaocup.compose.sample.semantics.tintColorSemantic @Composable fun MainScreen() { + var tintColor by remember { mutableStateOf(Color.Black) } + var iconRes by remember { mutableIntStateOf(R.drawable.ic_android) } + var iconImageVector by remember { mutableStateOf(Icons.Filled.AccountCircle) } + Column( modifier = Modifier .fillMaxSize() @@ -39,6 +54,41 @@ fun MainScreen() { .semantics { testTag = "mySimpleText" } ) + Row { + Icon( + modifier = Modifier + .imageContentSemantic(iconRes) + .tintColorSemantic(tintColor) + .testTag("iconDrawableRes"), + painter = painterResource(id = iconRes), + tint = tintColor, + contentDescription = null + ) + Icon( + modifier = Modifier + .imageContentSemantic(iconImageVector) + .tintColorSemantic(tintColor) + .testTag("iconImageVector"), + imageVector = iconImageVector, + tint = tintColor, + contentDescription = null + ) + } + + Button( + content = { + Text(text = stringResource(R.string.button_change_icon)) + }, + onClick = { + tintColor = Color.Blue + iconRes = R.drawable.ic_adb + iconImageVector = Icons.Filled.Call + }, + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "changeIconButton" } + ) + Button( content = { Text(text = stringResource(R.string.button_1)) diff --git a/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/Color.kt b/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/Color.kt new file mode 100644 index 00000000..1ad0a2fe --- /dev/null +++ b/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/Color.kt @@ -0,0 +1,13 @@ +package io.github.kakaocup.compose.sample.semantics + +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.semantics + +val TintColorSemanticKey = SemanticsPropertyKey("TintColor") +var SemanticsPropertyReceiver.tintColor by TintColorSemanticKey +fun Modifier.tintColorSemantic(color: Color): Modifier { + return semantics { tintColor = color } +} \ No newline at end of file diff --git a/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/Image.kt b/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/Image.kt new file mode 100644 index 00000000..e076924f --- /dev/null +++ b/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/Image.kt @@ -0,0 +1,12 @@ +package io.github.kakaocup.compose.sample.semantics + +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.semantics + +val ImageContentSemanticKey = SemanticsPropertyKey("ImageContent") +var SemanticsPropertyReceiver.imageContent by ImageContentSemanticKey +fun Modifier.imageContentSemantic(imageContent: Any): Modifier { + return semantics { this.imageContent = imageContent } +} \ No newline at end of file diff --git a/sample/src/main/res/drawable/ic_adb.xml b/sample/src/main/res/drawable/ic_adb.xml new file mode 100644 index 00000000..3e733b3c --- /dev/null +++ b/sample/src/main/res/drawable/ic_adb.xml @@ -0,0 +1,5 @@ + + + diff --git a/sample/src/main/res/drawable/ic_android.xml b/sample/src/main/res/drawable/ic_android.xml new file mode 100644 index 00000000..fe512307 --- /dev/null +++ b/sample/src/main/res/drawable/ic_android.xml @@ -0,0 +1,5 @@ + + + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index e61e4dca..a8a95e71 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ KakaoCompose Button 1 + Change icon \ No newline at end of file