diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TextColorAssertions.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TextColorAssertions.kt new file mode 100644 index 00000000..2cb7cda7 --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TextColorAssertions.kt @@ -0,0 +1,93 @@ +package io.github.kakaocup.compose.node.assertion + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsProperties +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 androidx.compose.ui.text.TextLayoutResult +import io.github.kakaocup.compose.utilities.ColorAssertionsUtils + +interface TextColorAssertions : NodeAssertions { + val textColorSemanticsPropertyKey: SemanticsPropertyKey + + /** + * Asserts that the text color contains the given [color]. + * + * Throws [AssertionError] if the text color value is not equal to `color`. + * Throws [IllegalStateException] if the compose view does not contain the [textColorSemanticsPropertyKey] modifier. + */ + fun assertTextColorEquals(color: Color) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { + assert(hasTextColor(color)) + } + } + + /** + * Asserts that text color contains the given [color]. + * + * Throws [AssertionError] if the text color value is not equal to `color`. + * Throws [IllegalStateException] if the compose view does not contain the [textColorSemanticsPropertyKey] modifier. + * Throws [IllegalArgumentException] if the color value is incorrect. + */ + fun assertTextColorEquals(color: String) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { + assert(hasTextColor(color)) + } + } + + /** + * Asserts that the text color contains the given [color]. + * + * Throws [AssertionError] if the text color value is not equal to `color`. + * Throws [IllegalStateException] if the compose view does not contain the [textColorSemanticsPropertyKey] modifier. + */ + fun assertTextColorEquals(color: Long) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { + assert(hasTextColor(color)) + } + } + + private fun hasTextColor(expectedColor: Color): SemanticsMatcher = SemanticsMatcher( + "${SemanticsProperties.Text.name} is of color '$expectedColor'" + ) { node -> + return@SemanticsMatcher node.hasTextColor(expectedColor = expectedColor) + } + + private fun hasTextColor(expectedColor: Long): SemanticsMatcher = SemanticsMatcher( + "${SemanticsProperties.Text.name} is of color '$expectedColor'" + ) { node -> + return@SemanticsMatcher node.hasTextColor(expectedColor = Color(expectedColor)) + } + + private fun hasTextColor(expectedColor: String): SemanticsMatcher = SemanticsMatcher( + "${SemanticsProperties.Text.name} is of color '$expectedColor'" + ) { node -> + return@SemanticsMatcher node.hasTextColor(expectedColor = ColorAssertionsUtils.getComposeColor(expectedColor)) + } + + private fun SemanticsNode.hasTextColor(expectedColor: Color): Boolean { + val textLayoutResults = mutableListOf() + config.getOrNull(SemanticsActions.GetTextLayoutResult)?.action?.invoke(textLayoutResults) + + val actualColorOfStyle = textLayoutResults.firstOrNull()?.layoutInput?.style?.color ?: Color.Unspecified + val actualColorOfSemantic = config.getOrNull(textColorSemanticsPropertyKey) ?: Color.Unspecified + + /** + * look at how the overrideColor parameter is used + * @see [androidx.compose.foundation.text.modifiers.TextStringSimpleNode.draw] + * @see [androidx.compose.foundation.text.modifiers.TextAnnotatedStringNode.draw] + */ + val actualColor = when { + actualColorOfSemantic.isSpecified -> actualColorOfSemantic + actualColorOfStyle.isSpecified -> actualColorOfStyle + else -> Color.Black + } + + return actualColor == expectedColor + } +} 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 index 762d073d..bdf420ea 100644 --- 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 @@ -2,10 +2,8 @@ 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 +import io.github.kakaocup.compose.utilities.ColorAssertionsUtils interface TintColorAssertions : NodeAssertions { val tintColorSemanticsPropertyKey: SemanticsPropertyKey @@ -17,7 +15,9 @@ interface TintColorAssertions : NodeAssertions { * 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)) } + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { + assert(ColorAssertionsUtils.hasColor(color, tintColorSemanticsPropertyKey)) + } } /** @@ -28,7 +28,9 @@ interface TintColorAssertions : NodeAssertions { * Throws [IllegalArgumentException] if the color value is incorrect. */ fun assertTintColorEquals(color: String) { - delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { assert(hasColor(color)) } + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { + assert(ColorAssertionsUtils.hasColor(color, tintColorSemanticsPropertyKey)) + } } /** @@ -38,33 +40,8 @@ interface TintColorAssertions : NodeAssertions { * 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) + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { + assert(ColorAssertionsUtils.hasColor(color, tintColorSemanticsPropertyKey)) + } } } diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KTextNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KTextNode.kt new file mode 100644 index 00000000..30e407e2 --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KTextNode.kt @@ -0,0 +1,17 @@ +package io.github.kakaocup.compose.node.element + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.node.assertion.TextColorAssertions +import io.github.kakaocup.compose.node.builder.NodeMatcher +import io.github.kakaocup.compose.node.core.BaseNode + +abstract class KTextNode( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>?, + useUnmergedTree: Boolean = false +) : BaseNode( + semanticsProvider = semanticsProvider, + nodeMatcher = nodeMatcher.copy(useUnmergedTree = useUnmergedTree), + parentNode = parentNode +), TextColorAssertions diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ColorAssertionsUtils.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ColorAssertionsUtils.kt new file mode 100644 index 00000000..fb22f2db --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ColorAssertionsUtils.kt @@ -0,0 +1,40 @@ +package io.github.kakaocup.compose.utilities + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.test.SemanticsMatcher + +internal object ColorAssertionsUtils { + fun hasColor(expectedColor: Color, semanticsPropertyKey: SemanticsPropertyKey<*>): SemanticsMatcher = SemanticsMatcher( + "The color is expected to be $expectedColor, but the actual color is different" + ) { node -> + val actualColor = node.config.getOrNull(semanticsPropertyKey) + ?: error("Compose view does not contain $semanticsPropertyKey modifier") + + return@SemanticsMatcher actualColor == expectedColor + } + + fun hasColor(expectedColor: String, semanticsPropertyKey: SemanticsPropertyKey<*>): SemanticsMatcher = SemanticsMatcher( + "The color is expected to be $expectedColor, but the actual color is different" + ) { node -> + val actualColor = node.config.getOrNull(semanticsPropertyKey) + ?: error("Compose view does not contain $semanticsPropertyKey modifier") + + return@SemanticsMatcher actualColor == getComposeColor(expectedColor) + } + + fun hasColor(expectedColor: Long, semanticsPropertyKey: SemanticsPropertyKey<*>): SemanticsMatcher = SemanticsMatcher( + "The color is expected to be $expectedColor, but the actual color is different" + ) { node -> + val actualColor = node.config.getOrNull(semanticsPropertyKey) + ?: error("Compose view does not contain $semanticsPropertyKey modifier") + + return@SemanticsMatcher actualColor == Color(expectedColor) + } + + fun getComposeColor(color: String): Color { + val colorString = if (color.contains("#")) color else "#$color" + return Color(android.graphics.Color.parseColor(colorString)) + } +} 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 deleted file mode 100644 index 44efb21d..00000000 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ColorUtils.kt +++ /dev/null @@ -1,8 +0,0 @@ -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)) -} diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppTextNode.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppTextNode.kt new file mode 100644 index 00000000..f44b1b74 --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppTextNode.kt @@ -0,0 +1,22 @@ +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.KTextNode +import io.github.kakaocup.compose.sample.semantics.TextColorSemanticKey + +class KAppTextNode( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>? = null, +) : KTextNode( + semanticsProvider = semanticsProvider, + nodeMatcher = nodeMatcher, + parentNode = parentNode, + useUnmergedTree = true +) { + override val textColorSemanticsPropertyKey: SemanticsPropertyKey = TextColorSemanticKey +} 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 1f461cea..3a89e9b6 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 @@ -2,6 +2,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.KAppTextNode import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.KNode @@ -33,6 +34,22 @@ class MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : hasTestTag("iconImageVector") } + val changeTextColorButton: KNode = child { + hasTestTag("changeTextColorButton") + } + + val textWithoutStyle: KAppTextNode = child { + hasTestTag("textWithoutStyle") + } + + val textWithStyle: KAppTextNode = child { + hasTestTag("textWithStyle") + } + + val textWithSemantic: KAppTextNode = child { + hasTestTag("textWithSemantic") + } + 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 b09ec41b..34de80f4 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 @@ -89,4 +89,63 @@ class SimpleTest { } } } + + @Test + fun textColorTest() { + onComposeScreen(composeTestRule) { + changeTextColorButton { + assertIsDisplayed() + } + + textWithoutStyle { + assertIsDisplayed() + assertTextColorEquals(Color.Black) + assertTextColorEquals("000000") + assertTextColorEquals("#000000") + assertTextColorEquals(0xFF000000) + } + + textWithStyle { + assertIsDisplayed() + assertTextColorEquals(Color.Black) + assertTextColorEquals("000000") + assertTextColorEquals("#000000") + assertTextColorEquals(0xFF000000) + } + + textWithSemantic { + assertIsDisplayed() + assertTextColorEquals(Color.Black) + assertTextColorEquals("000000") + assertTextColorEquals("#000000") + assertTextColorEquals(0xFF000000) + } + + changeTextColorButton.performClick() + + textWithoutStyle { + assertIsDisplayed() + assertTextColorEquals(Color.Black) + assertTextColorEquals("000000") + assertTextColorEquals("#000000") + assertTextColorEquals(0xFF000000) + } + + textWithStyle { + assertIsDisplayed() + assertTextColorEquals(Color.Blue) + assertTextColorEquals("0000FF") + assertTextColorEquals("#0000FF") + assertTextColorEquals(0xFF0000FF) + } + + textWithSemantic { + assertIsDisplayed() + assertTextColorEquals(Color.Blue) + assertTextColorEquals("0000FF") + assertTextColorEquals("#0000FF") + assertTextColorEquals(0xFF0000FF) + } + } + } } 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 de469abf..ac9a9a1b 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 @@ -27,11 +27,13 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.github.kakaocup.compose.sample.semantics.imageContentSemantic +import io.github.kakaocup.compose.sample.semantics.textColorSemantic import io.github.kakaocup.compose.sample.semantics.tintColorSemantic @Composable fun MainScreen() { var tintColor by remember { mutableStateOf(Color.Black) } + var textColor by remember { mutableStateOf(Color.Black) } var iconRes by remember { mutableIntStateOf(R.drawable.ic_android) } var iconImageVector by remember { mutableStateOf(Icons.Filled.AccountCircle) } @@ -89,6 +91,45 @@ fun MainScreen() { .semantics { testTag = "changeIconButton" } ) + Text( + text = "Text without style", + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "textWithoutStyle" } + ) + + Text( + text = "Text with style", + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "textWithStyle" } + .textColorSemantic(textColor), + style = MaterialTheme.typography.body1.copy( + color = textColor + ) + ) + + Text( + text = "Text with semantic property", + color = textColor, + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "textWithSemantic" } + .textColorSemantic(textColor) + ) + + Button( + content = { + Text(text = stringResource(R.string.button_change_text_color)) + }, + onClick = { + textColor = Color.Blue + }, + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "changeTextColorButton" } + ) + 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 index 7a3a8df3..3969f8fc 100644 --- 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 @@ -11,3 +11,9 @@ var SemanticsPropertyReceiver.tintColor by TintColorSemanticKey fun Modifier.tintColorSemantic(color: Color): Modifier { return semantics { tintColor = color } } + +val TextColorSemanticKey = SemanticsPropertyKey("TextColor") +var SemanticsPropertyReceiver.textColor by TextColorSemanticKey +fun Modifier.textColorSemantic(color: Color): Modifier { + return semantics { textColor = color } +} diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 34793b88..6edf1016 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -2,4 +2,5 @@ KakaoCompose Button 1 Change icon + Change text color