From fa920e6c1813fc067edb0f5235c0bce40f6bf263 Mon Sep 17 00:00:00 2001 From: Konstantin Aksenov Date: Tue, 4 Jun 2024 22:31:07 +1000 Subject: [PATCH 01/20] feat(core): override global useUnmergedTree --- .../kotlin/io/github/kakaocup/compose/KakaoCompose.kt | 11 ++++++++++- .../kakaocup/compose/node/builder/NodeMatcher.kt | 3 ++- .../kakaocup/compose/node/builder/ViewBuilder.kt | 3 ++- .../github/kakaocup/compose/node/element/KIconNode.kt | 3 ++- .../github/kakaocup/compose/node/element/KTextNode.kt | 3 ++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt index bb347b42..e9e603a5 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt @@ -6,7 +6,8 @@ import io.github.kakaocup.compose.intercept.operation.ComposeAction import io.github.kakaocup.compose.intercept.operation.ComposeAssertion object KakaoCompose { - internal var composeInterceptor: Interceptor? = null + internal var composeInterceptor: Interceptor? = + null /** * Operator that allows usage of DSL style @@ -37,6 +38,14 @@ object KakaoCompose { * @see intercept * @see Interceptor */ + + /** + * Global overrides for default Espresso behaviour + */ + object Override { + var useUnmergedTree: Boolean? = null + } + fun reset() { composeInterceptor = null } diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/NodeMatcher.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/NodeMatcher.kt index f5955445..ae1f157a 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/NodeMatcher.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/NodeMatcher.kt @@ -1,9 +1,10 @@ package io.github.kakaocup.compose.node.builder import androidx.compose.ui.test.SemanticsMatcher +import io.github.kakaocup.compose.KakaoCompose data class NodeMatcher( val matcher: SemanticsMatcher, val position: Int = 0, - val useUnmergedTree: Boolean = false, + val useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false, ) \ No newline at end of file diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt index 5fb99155..4987a494 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt @@ -4,6 +4,7 @@ import androidx.annotation.StringRes import androidx.compose.ui.semantics.* import androidx.compose.ui.test.* import androidx.compose.ui.text.input.ImeAction +import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.node.core.ComposeMarker import io.github.kakaocup.compose.utilities.getResourceString @@ -14,7 +15,7 @@ class ViewBuilder { private var position = 0 - var useUnmergedTree: Boolean = false + var useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false fun isEnabled() = addFilter(androidx.compose.ui.test.isEnabled()) 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 index db9d9165..dfd170b8 100644 --- 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 @@ -1,6 +1,7 @@ package io.github.kakaocup.compose.node.element import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.node.assertion.ImageContentAssertions import io.github.kakaocup.compose.node.assertion.TintColorAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher @@ -10,7 +11,7 @@ abstract class KIconNode( semanticsProvider: SemanticsNodeInteractionsProvider, nodeMatcher: NodeMatcher, parentNode: BaseNode<*>?, - useUnmergedTree: Boolean = false + useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false ) : BaseNode( semanticsProvider = semanticsProvider, nodeMatcher = nodeMatcher.copy(useUnmergedTree = useUnmergedTree), 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 index 30e407e2..848c625d 100644 --- 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 @@ -1,6 +1,7 @@ package io.github.kakaocup.compose.node.element import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.node.assertion.TextColorAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.core.BaseNode @@ -9,7 +10,7 @@ abstract class KTextNode( semanticsProvider: SemanticsNodeInteractionsProvider, nodeMatcher: NodeMatcher, parentNode: BaseNode<*>?, - useUnmergedTree: Boolean = false + useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false ) : BaseNode( semanticsProvider = semanticsProvider, nodeMatcher = nodeMatcher.copy(useUnmergedTree = useUnmergedTree), From 2a5468a5e089e81718d55ecb609b8796ccc084f0 Mon Sep 17 00:00:00 2001 From: Konstantin Aksenov Date: Tue, 4 Jun 2024 22:45:50 +1000 Subject: [PATCH 02/20] docs(readme): override global useUnmergedTree --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 080be9e0..a4b0a388 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,10 @@ class SomeTest { ``` For more detailed info please refer to the documentation. +### Useful tips: +By default Espresso using `useUnmergedTree = true` and it create a lot of inconveniences with node matching. +However you can override global parameter `KakaoCompose.Override.useUnmergedTree = false` in single place for all tests + ### Setup Maven ```xml From bff2ea98cf53ed3e692c7d4108ea9590ef5f92b8 Mon Sep 17 00:00:00 2001 From: Konstantin Aksenov Date: Tue, 11 Jun 2024 11:55:21 +1000 Subject: [PATCH 03/20] feat(core): add global semantics provider --- .../github/kakaocup/compose/KakaoCompose.kt | 15 ++++++++ .../kakaocup/compose/node/core/BaseNode.kt | 9 +++-- .../compose/node/element/ComposeScreen.kt | 38 ++++++++++++------- .../compose/node/element/KIconNode.kt | 2 +- .../kakaocup/compose/node/element/KNode.kt | 7 ++-- .../compose/node/element/KTextNode.kt | 2 +- .../element/lazylist/KLazyListItemNode.kt | 2 +- .../node/element/lazylist/KLazyListNode.kt | 19 ++++++++-- .../compose/rule/KakaoComposeTestRule.kt | 21 ++++++++++ .../kakaocup/compose/node/KAppIconNode.kt | 2 +- .../kakaocup/compose/node/KAppTextNode.kt | 2 +- .../MainActivityGlobalSemanticScreen.kt | 17 +++++++++ .../compose/test/SimpleTestGlobalSemantic.kt | 28 ++++++++++++++ 13 files changed, 136 insertions(+), 28 deletions(-) create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/screen/MainActivityGlobalSemanticScreen.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTestGlobalSemantic.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt index e9e603a5..75bcaf88 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt @@ -1,6 +1,8 @@ package io.github.kakaocup.compose +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import io.github.kakaocup.compose.intercept.base.Interceptor +import io.github.kakaocup.compose.rule.KakaoComposeTestRule import io.github.kakaocup.compose.intercept.interaction.ComposeInteraction import io.github.kakaocup.compose.intercept.operation.ComposeAction import io.github.kakaocup.compose.intercept.operation.ComposeAssertion @@ -46,6 +48,19 @@ object KakaoCompose { var useUnmergedTree: Boolean? = null } + /** + * Global parameters + */ + + object Global { + /** + * Global SemanticsNodeInteractionsProvider can be set via KakaoComposeTestRule + * to avoid injection boilerplate into each ComposeScreen + * @see KakaoComposeTestRule + */ + var semanticsProvider: SemanticsNodeInteractionsProvider? = null + } + fun reset() { composeInterceptor = null } diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index c0d6d722..a64a70de 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -3,6 +3,7 @@ package io.github.kakaocup.compose.node.core import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.hasAnyAncestor +import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.intercept.delegate.ComposeDelegate import io.github.kakaocup.compose.intercept.delegate.ComposeInterceptable import io.github.kakaocup.compose.node.action.NodeActions @@ -15,7 +16,7 @@ import io.github.kakaocup.compose.node.builder.ViewBuilder @ComposeMarker abstract class BaseNode> constructor( - @PublishedApi internal val semanticsProvider: SemanticsNodeInteractionsProvider, + @PublishedApi internal val semanticsProvider: SemanticsNodeInteractionsProvider? = null, private val nodeMatcher: NodeMatcher, private val parentNode: BaseNode<*>? = null, ) : KDSL, @@ -25,8 +26,9 @@ abstract class BaseNode> constructor( TextActions, ComposeInterceptable { + constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, viewBuilderAction: ViewBuilder.() -> Unit, ) : this( semanticsProvider = semanticsProvider, @@ -35,7 +37,7 @@ abstract class BaseNode> constructor( ) constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, nodeMatcher: NodeMatcher, ) : this( semanticsProvider = semanticsProvider, @@ -44,6 +46,7 @@ abstract class BaseNode> constructor( ) override val delegate: ComposeDelegate by lazy(LazyThreadSafetyMode.NONE) { + val semanticsProvider = requireNotNull(semanticsProvider ?: KakaoCompose.Global.semanticsProvider) { "SemanticsProvider not is null: Provide via constructor or use KakaoComposeTestRule" } ComposeDelegate( nodeProvider = NodeProvider( nodeMatcher = NodeMatcher( diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt index c13a9acd..8d8148b1 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt @@ -2,6 +2,7 @@ package io.github.kakaocup.compose.node.element import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.node.core.ComposeMarker import io.github.kakaocup.compose.node.core.BaseNode import io.github.kakaocup.compose.node.builder.NodeMatcher @@ -12,39 +13,50 @@ import io.github.kakaocup.compose.node.builder.ViewBuilder open class ComposeScreen> : BaseNode { internal constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, nodeMatcher: NodeMatcher, parentNode: ComposeScreen? = null, ) : super(semanticsProvider, nodeMatcher, parentNode) constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, viewBuilderAction: ViewBuilder.() -> Unit, ) : super(semanticsProvider, viewBuilderAction) constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, nodeMatcher: NodeMatcher = NodeMatcher( matcher = SemanticsMatcher( description = "Empty matcher", - matcher = { true} + matcher = { true } ) ), ) : super(semanticsProvider, nodeMatcher) - fun onNode(viewBuilderAction: ViewBuilder.() -> Unit) = KNode(semanticsProvider, viewBuilderAction) + fun onNode(viewBuilderAction: ViewBuilder.() -> Unit) = KNode( + requireNotNull( + semanticsProvider ?: KakaoCompose.Global.semanticsProvider + ) { "SemanticsProvider not is null: Provide via constructor or use KakaoComposeTestRule" }, + viewBuilderAction, + ) companion object { inline fun > onComposeScreen( semanticsProvider: SemanticsNodeInteractionsProvider, noinline function: T.() -> Unit - ): T { - return T::class.java - .getDeclaredConstructor( - SemanticsNodeInteractionsProvider::class.java - ) - .newInstance(semanticsProvider) + ): T = T::class.java + .getDeclaredConstructor( + SemanticsNodeInteractionsProvider::class.java + ) + .newInstance(semanticsProvider) + .apply { this(function) } + + inline fun > onComposeScreen( + noinline function: T.() -> Unit + ): T = + T::class.java.getDeclaredConstructor() + .newInstance() .apply { this(function) } - } + } -} \ 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 index dfd170b8..26371701 100644 --- 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 @@ -8,7 +8,7 @@ import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.core.BaseNode abstract class KIconNode( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, nodeMatcher: NodeMatcher, parentNode: BaseNode<*>?, useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KNode.kt index 35cadfca..df041f45 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KNode.kt @@ -1,6 +1,7 @@ package io.github.kakaocup.compose.node.element import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder import io.github.kakaocup.compose.node.core.BaseNode @@ -8,18 +9,18 @@ import io.github.kakaocup.compose.node.core.BaseNode open class KNode : BaseNode { constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, nodeMatcher: NodeMatcher, parentNode: BaseNode<*>? = null, ) : super(semanticsProvider, nodeMatcher, parentNode) constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, viewBuilderAction: ViewBuilder.() -> Unit, ) : super(semanticsProvider, viewBuilderAction) constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, nodeMatcher: NodeMatcher, ) : super(semanticsProvider, nodeMatcher) } \ No newline at end of file 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 index 848c625d..3eb48e3d 100644 --- 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 @@ -7,7 +7,7 @@ import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.core.BaseNode abstract class KTextNode( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, nodeMatcher: NodeMatcher, parentNode: BaseNode<*>?, useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemNode.kt index 47cd20da..84efbbc7 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemNode.kt @@ -16,7 +16,7 @@ import io.github.kakaocup.compose.node.core.BaseNode */ abstract class KLazyListItemNode>( semanticNode: SemanticsNode, - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, ) : BaseNode( semanticsProvider, NodeMatcher( diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt index a25b547b..dad7aaa2 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.filter import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.onChildren +import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.node.assertion.LazyListNodeAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder @@ -16,7 +17,7 @@ import io.github.kakaocup.compose.node.core.BaseNode * Node class with special api to test Lazy List (LazyColumn or LazyRow) */ class KLazyListNode( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, nodeMatcher: NodeMatcher, itemTypeBuilder: KLazyListItemBuilder.() -> Unit, val positionMatcher: (position: Int) -> SemanticsMatcher, @@ -37,7 +38,7 @@ class KLazyListNode( * @see ViewBuilder */ constructor( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider? = null, viewBuilderAction: ViewBuilder.() -> Unit, itemTypeBuilder: KLazyListItemBuilder.() -> Unit, positionMatcher: (position: Int) -> SemanticsMatcher, @@ -68,13 +69,18 @@ class KLazyListNode( performScrollToIndex(position) + val semanticsProvider = requireNotNull(semanticsProvider ?: KakaoCompose.Global.semanticsProvider) { "SemanticsProvider not is null: Provide via constructor or use KakaoComposeTestRule" } + val semanticsNode = semanticsProvider .onNode(semanticsMatcher) .onChildren() .filterToOne(positionMatcher(position)) .fetchSemanticsNode() - function(provideItem(semanticsNode, semanticsProvider) as T) + function(provideItem( + semanticsNode, + semanticsProvider + ) as T) } /** @@ -94,13 +100,18 @@ class KLazyListNode( performScrollToNode(nodeMatcher.matcher) + val semanticsProvider = requireNotNull(semanticsProvider ?: KakaoCompose.Global.semanticsProvider) { "SemanticsProvider not is null: Provide via constructor or use KakaoComposeTestRule" } + val semanticsNode = semanticsProvider .onNode(semanticsMatcher) .onChildren() .filter(nodeMatcher.matcher)[nodeMatcher.position] .fetchSemanticsNode() - return provideItem(semanticsNode, semanticsProvider) as T + return provideItem( + semanticsNode, + semanticsProvider + ) as T } /** diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt new file mode 100644 index 00000000..05da9e80 --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt @@ -0,0 +1,21 @@ +package io.github.kakaocup.compose.rule + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.KakaoCompose +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class KakaoComposeTestRule( + val semanticsProvider: SemanticsNodeInteractionsProvider?, +) : TestRule { + override fun apply(base: Statement, description: Description): Statement = + object : Statement() { + override fun evaluate() { + val oldSemanticsProvided = KakaoCompose.Global.semanticsProvider + KakaoCompose.Global.semanticsProvider = semanticsProvider + base.evaluate() + KakaoCompose.Global.semanticsProvider = oldSemanticsProvided + } + } +} 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 index b24701a8..98b92430 100644 --- a/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppIconNode.kt +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppIconNode.kt @@ -10,7 +10,7 @@ import io.github.kakaocup.compose.sample.semantics.ImageContentSemanticKey import io.github.kakaocup.compose.sample.semantics.TintColorSemanticKey class KAppIconNode( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider?, nodeMatcher: NodeMatcher, parentNode: BaseNode<*>? = null, ) : KIconNode( 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 index f44b1b74..8977b453 100644 --- a/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppTextNode.kt +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppTextNode.kt @@ -9,7 +9,7 @@ import io.github.kakaocup.compose.node.element.KTextNode import io.github.kakaocup.compose.sample.semantics.TextColorSemanticKey class KAppTextNode( - semanticsProvider: SemanticsNodeInteractionsProvider, + semanticsProvider: SemanticsNodeInteractionsProvider?, nodeMatcher: NodeMatcher, parentNode: BaseNode<*>? = null, ) : KTextNode( diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/screen/MainActivityGlobalSemanticScreen.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/MainActivityGlobalSemanticScreen.kt new file mode 100644 index 00000000..021a263c --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/MainActivityGlobalSemanticScreen.kt @@ -0,0 +1,17 @@ +package io.github.kakaocup.compose.screen + +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 + +class MainActivityGlobalSemanticScreen : + ComposeScreen( + viewBuilderAction = { hasTestTag("MainScreen") } +) { + + val myText1: KNode = child { + hasTestTag("mySimpleText") + hasPosition(0) + } +} diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTestGlobalSemantic.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTestGlobalSemantic.kt new file mode 100644 index 00000000..f9c2ee2d --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/test/SimpleTestGlobalSemantic.kt @@ -0,0 +1,28 @@ +package io.github.kakaocup.compose.test + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.rule.KakaoComposeTestRule +import io.github.kakaocup.compose.sample.MainActivity +import io.github.kakaocup.compose.screen.MainActivityGlobalSemanticScreen +import org.junit.Rule +import org.junit.Test + +class SimpleTestGlobalSemantic { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @get:Rule + val kakaoComposeTestRule = KakaoComposeTestRule(composeTestRule) + + @Test + fun simpleTest() { + onComposeScreen { + myText1 { + assertIsDisplayed() + assertTextContains("Simple text 1") + } + } + } +} From 7d0eddb504c759939597bb2cc6d59d379178cbe5 Mon Sep 17 00:00:00 2001 From: Konstantin Aksenov Date: Tue, 11 Jun 2024 20:36:56 +1000 Subject: [PATCH 04/20] feat(core): add extension for global sematic provider usage --- .../compose/exception/KakaoComposeException.kt | 3 +++ .../kakaocup/compose/node/core/BaseNode.kt | 5 +++-- .../compose/node/element/ComposeScreen.kt | 6 +++--- .../node/element/lazylist/KLazyListNode.kt | 16 +++++++++++----- .../compose/rule/KakaoComposeTestRule.kt | 6 +++++- .../kakaocup/compose/utilities/Extensions.kt | 11 +++++++++++ 6 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/exception/KakaoComposeException.kt create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/exception/KakaoComposeException.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/exception/KakaoComposeException.kt new file mode 100644 index 00000000..5b2d0295 --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/exception/KakaoComposeException.kt @@ -0,0 +1,3 @@ +package io.github.kakaocup.compose.exception + +class KakaoComposeException(message: String) : Exception(message) \ No newline at end of file diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index a64a70de..68e37984 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -13,6 +13,8 @@ import io.github.kakaocup.compose.node.assertion.TextResourcesNodeAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.NodeProvider import io.github.kakaocup.compose.node.builder.ViewBuilder +import io.github.kakaocup.compose.utilities.checkNotNull +import io.github.kakaocup.compose.utilities.orGlobal @ComposeMarker abstract class BaseNode> constructor( @@ -46,7 +48,6 @@ abstract class BaseNode> constructor( ) override val delegate: ComposeDelegate by lazy(LazyThreadSafetyMode.NONE) { - val semanticsProvider = requireNotNull(semanticsProvider ?: KakaoCompose.Global.semanticsProvider) { "SemanticsProvider not is null: Provide via constructor or use KakaoComposeTestRule" } ComposeDelegate( nodeProvider = NodeProvider( nodeMatcher = NodeMatcher( @@ -54,7 +55,7 @@ abstract class BaseNode> constructor( position = nodeMatcher.position, useUnmergedTree = nodeMatcher.useUnmergedTree ), - semanticsProvider = semanticsProvider + semanticsProvider = semanticsProvider.orGlobal().checkNotNull() ), parentDelegate = parentNode?.delegate ) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt index 8d8148b1..cea2d4a2 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt @@ -7,6 +7,8 @@ import io.github.kakaocup.compose.node.core.ComposeMarker import io.github.kakaocup.compose.node.core.BaseNode import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder +import io.github.kakaocup.compose.utilities.checkNotNull +import io.github.kakaocup.compose.utilities.orGlobal @Suppress("UNCHECKED_CAST") @ComposeMarker @@ -34,9 +36,7 @@ open class ComposeScreen> : BaseNode { ) : super(semanticsProvider, nodeMatcher) fun onNode(viewBuilderAction: ViewBuilder.() -> Unit) = KNode( - requireNotNull( - semanticsProvider ?: KakaoCompose.Global.semanticsProvider - ) { "SemanticsProvider not is null: Provide via constructor or use KakaoComposeTestRule" }, + semanticsProvider.orGlobal().checkNotNull(), viewBuilderAction, ) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt index dad7aaa2..573f4b33 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt @@ -8,10 +8,13 @@ import androidx.compose.ui.test.filter import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.onChildren import io.github.kakaocup.compose.KakaoCompose +import io.github.kakaocup.compose.exception.KakaoComposeException import io.github.kakaocup.compose.node.assertion.LazyListNodeAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder import io.github.kakaocup.compose.node.core.BaseNode +import io.github.kakaocup.compose.utilities.checkNotNull +import io.github.kakaocup.compose.utilities.orGlobal /** * Node class with special api to test Lazy List (LazyColumn or LazyRow) @@ -68,10 +71,9 @@ class KLazyListNode( }.provideItem performScrollToIndex(position) - - val semanticsProvider = requireNotNull(semanticsProvider ?: KakaoCompose.Global.semanticsProvider) { "SemanticsProvider not is null: Provide via constructor or use KakaoComposeTestRule" } - val semanticsNode = semanticsProvider + .orGlobal() + .checkNotNull() .onNode(semanticsMatcher) .onChildren() .filterToOne(positionMatcher(position)) @@ -80,6 +82,8 @@ class KLazyListNode( function(provideItem( semanticsNode, semanticsProvider + .orGlobal() + .checkNotNull() ) as T) } @@ -100,9 +104,9 @@ class KLazyListNode( performScrollToNode(nodeMatcher.matcher) - val semanticsProvider = requireNotNull(semanticsProvider ?: KakaoCompose.Global.semanticsProvider) { "SemanticsProvider not is null: Provide via constructor or use KakaoComposeTestRule" } - val semanticsNode = semanticsProvider + .orGlobal() + .checkNotNull() .onNode(semanticsMatcher) .onChildren() .filter(nodeMatcher.matcher)[nodeMatcher.position] @@ -111,6 +115,8 @@ class KLazyListNode( return provideItem( semanticsNode, semanticsProvider + .orGlobal() + .checkNotNull() ) as T } diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt index 05da9e80..1526844f 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt @@ -1,13 +1,17 @@ package io.github.kakaocup.compose.rule +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionCollection import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.junit4.ComposeContentTestRule import io.github.kakaocup.compose.KakaoCompose import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement class KakaoComposeTestRule( - val semanticsProvider: SemanticsNodeInteractionsProvider?, + val semanticsProvider: SemanticsNodeInteractionsProvider, ) : TestRule { override fun apply(base: Statement, description: Description): Statement = object : Statement() { diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt new file mode 100644 index 00000000..614ca258 --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt @@ -0,0 +1,11 @@ +package io.github.kakaocup.compose.utilities + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.KakaoCompose +import io.github.kakaocup.compose.exception.KakaoComposeException + +fun SemanticsNodeInteractionsProvider?.orGlobal(): SemanticsNodeInteractionsProvider? = + this ?: KakaoCompose.Global.semanticsProvider + +fun SemanticsNodeInteractionsProvider?.checkNotNull(): SemanticsNodeInteractionsProvider = + this ?: throw KakaoComposeException("SemanticsProvider is null: Provide via constructor or use KakaoComposeTestRule") \ No newline at end of file From d5f85c9c42a878345251f1b9a703dae06c59af7c Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 18:56:04 +0500 Subject: [PATCH 05/20] [klistnode] Adds allowed getter for SemanticsNodeInteractionsProvider --- .../kakaocup/compose/node/core/BaseNode.kt | 12 +++++++++--- .../compose/node/element/ComposeScreen.kt | 8 +++----- .../node/element/lazylist/KLazyListNode.kt | 18 ++++-------------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index 68e37984..56d7d97f 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -3,7 +3,6 @@ package io.github.kakaocup.compose.node.core import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.hasAnyAncestor -import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.intercept.delegate.ComposeDelegate import io.github.kakaocup.compose.intercept.delegate.ComposeInterceptable import io.github.kakaocup.compose.node.action.NodeActions @@ -28,7 +27,6 @@ abstract class BaseNode> constructor( TextActions, ComposeInterceptable { - constructor( semanticsProvider: SemanticsNodeInteractionsProvider? = null, viewBuilderAction: ViewBuilder.() -> Unit, @@ -55,7 +53,7 @@ abstract class BaseNode> constructor( position = nodeMatcher.position, useUnmergedTree = nodeMatcher.useUnmergedTree ), - semanticsProvider = semanticsProvider.orGlobal().checkNotNull() + semanticsProvider = getSemanticsProvider() ), parentDelegate = parentNode?.delegate ) @@ -73,6 +71,14 @@ abstract class BaseNode> constructor( ) } + /** + * Allowed getter for [semanticsProvider]. + * Any [SemanticsNodeInteractionsProvider] must be initialized before use. + */ + fun getSemanticsProvider(): SemanticsNodeInteractionsProvider { + return this.semanticsProvider.orGlobal().checkNotNull() + } + /*** * Combines semantic matchers from all ancestor nodes */ diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt index cea2d4a2..167e91cf 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt @@ -7,8 +7,6 @@ import io.github.kakaocup.compose.node.core.ComposeMarker import io.github.kakaocup.compose.node.core.BaseNode import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder -import io.github.kakaocup.compose.utilities.checkNotNull -import io.github.kakaocup.compose.utilities.orGlobal @Suppress("UNCHECKED_CAST") @ComposeMarker @@ -36,14 +34,14 @@ open class ComposeScreen> : BaseNode { ) : super(semanticsProvider, nodeMatcher) fun onNode(viewBuilderAction: ViewBuilder.() -> Unit) = KNode( - semanticsProvider.orGlobal().checkNotNull(), + getSemanticsProvider(), viewBuilderAction, ) companion object { inline fun > onComposeScreen( semanticsProvider: SemanticsNodeInteractionsProvider, - noinline function: T.() -> Unit + noinline function: T.() -> Unit, ): T = T::class.java .getDeclaredConstructor( SemanticsNodeInteractionsProvider::class.java @@ -52,7 +50,7 @@ open class ComposeScreen> : BaseNode { .apply { this(function) } inline fun > onComposeScreen( - noinline function: T.() -> Unit + noinline function: T.() -> Unit, ): T = T::class.java.getDeclaredConstructor() .newInstance() diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt index 573f4b33..88b42a32 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt @@ -13,8 +13,6 @@ import io.github.kakaocup.compose.node.assertion.LazyListNodeAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder import io.github.kakaocup.compose.node.core.BaseNode -import io.github.kakaocup.compose.utilities.checkNotNull -import io.github.kakaocup.compose.utilities.orGlobal /** * Node class with special api to test Lazy List (LazyColumn or LazyRow) @@ -71,9 +69,7 @@ class KLazyListNode( }.provideItem performScrollToIndex(position) - val semanticsNode = semanticsProvider - .orGlobal() - .checkNotNull() + val semanticsNode = getSemanticsProvider() .onNode(semanticsMatcher) .onChildren() .filterToOne(positionMatcher(position)) @@ -81,9 +77,7 @@ class KLazyListNode( function(provideItem( semanticsNode, - semanticsProvider - .orGlobal() - .checkNotNull() + getSemanticsProvider() ) as T) } @@ -104,9 +98,7 @@ class KLazyListNode( performScrollToNode(nodeMatcher.matcher) - val semanticsNode = semanticsProvider - .orGlobal() - .checkNotNull() + val semanticsNode = getSemanticsProvider() .onNode(semanticsMatcher) .onChildren() .filter(nodeMatcher.matcher)[nodeMatcher.position] @@ -114,9 +106,7 @@ class KLazyListNode( return provideItem( semanticsNode, - semanticsProvider - .orGlobal() - .checkNotNull() + getSemanticsProvider() ) as T } From e15dcd5ea6e272904d2f67d3f7d5de36636567bf Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 19:01:26 +0500 Subject: [PATCH 06/20] [klistnode] Adds allowed getter for NodeMatcher in BaseNode --- .../kakaocup/compose/node/core/BaseNode.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index 56d7d97f..e7fb718b 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -3,6 +3,7 @@ package io.github.kakaocup.compose.node.core import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.hasAnyAncestor +import io.github.kakaocup.compose.exception.KakaoComposeException import io.github.kakaocup.compose.intercept.delegate.ComposeDelegate import io.github.kakaocup.compose.intercept.delegate.ComposeInterceptable import io.github.kakaocup.compose.node.action.NodeActions @@ -18,7 +19,7 @@ import io.github.kakaocup.compose.utilities.orGlobal @ComposeMarker abstract class BaseNode> constructor( @PublishedApi internal val semanticsProvider: SemanticsNodeInteractionsProvider? = null, - private val nodeMatcher: NodeMatcher, + private val nodeMatcher: NodeMatcher? = null, private val parentNode: BaseNode<*>? = null, ) : KDSL, NodeAssertions, @@ -50,8 +51,8 @@ abstract class BaseNode> constructor( nodeProvider = NodeProvider( nodeMatcher = NodeMatcher( matcher = combineSemanticMatchers(), - position = nodeMatcher.position, - useUnmergedTree = nodeMatcher.useUnmergedTree + position = getNodeMatcher().position, + useUnmergedTree = getNodeMatcher().useUnmergedTree ), semanticsProvider = getSemanticsProvider() ), @@ -71,6 +72,15 @@ abstract class BaseNode> constructor( ) } + /** + * Allowed getter for [nodeMatcher]. + * Any [NodeMatcher] must be initialized before use. + */ + fun getNodeMatcher(): NodeMatcher { + return this.nodeMatcher + ?: throw KakaoComposeException("NodeMatcher is null: Provide via constructor or use `initSemantics` method`") + } + /** * Allowed getter for [semanticsProvider]. * Any [SemanticsNodeInteractionsProvider] must be initialized before use. @@ -87,10 +97,10 @@ abstract class BaseNode> constructor( var parent = this.parentNode while (parent != null) { - semanticsMatcherList.add(hasAnyAncestor(parent.nodeMatcher.matcher)) + semanticsMatcherList.add(hasAnyAncestor(parent.getNodeMatcher().matcher)) parent = parent.parentNode } - semanticsMatcherList.add(this.nodeMatcher.matcher) + semanticsMatcherList.add(this.getNodeMatcher().matcher) return semanticsMatcherList.reduce { finalMatcher, matcher -> finalMatcher and matcher } } From 4c1175bdbfd05f2cc9570e58121bb9e11037589f Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 19:02:04 +0500 Subject: [PATCH 07/20] [klistnode] Adds method for deffered initialization of BaseNode constuctor parameters --- .../kakaocup/compose/node/core/BaseNode.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index e7fb718b..0b2e92ef 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -18,9 +18,9 @@ import io.github.kakaocup.compose.utilities.orGlobal @ComposeMarker abstract class BaseNode> constructor( - @PublishedApi internal val semanticsProvider: SemanticsNodeInteractionsProvider? = null, - private val nodeMatcher: NodeMatcher? = null, - private val parentNode: BaseNode<*>? = null, + @PublishedApi internal var semanticsProvider: SemanticsNodeInteractionsProvider? = null, + private var nodeMatcher: NodeMatcher? = null, + private var parentNode: BaseNode<*>? = null, ) : KDSL, NodeAssertions, TextResourcesNodeAssertions, @@ -89,6 +89,20 @@ abstract class BaseNode> constructor( return this.semanticsProvider.orGlobal().checkNotNull() } + /** + * Method for deferred initialization of [BaseNode] constructor parameters. + * Simplifies the description of child nodes in list nodes. + */ + fun initSemantics( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>? = null, + ) { + this.semanticsProvider = semanticsProvider + this.nodeMatcher = nodeMatcher + this.parentNode = parentNode + } + /*** * Combines semantic matchers from all ancestor nodes */ From 39117d6d11e91466f5ab0bb48eae5e365c4376da Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 19:04:42 +0500 Subject: [PATCH 08/20] [klistnode] Adds KNode for list items --- .../node/element/list/KListItemNode.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt new file mode 100644 index 00000000..33b250cc --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt @@ -0,0 +1,52 @@ +package io.github.kakaocup.compose.node.element.list + +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.test.SemanticsMatcher +import io.github.kakaocup.compose.node.builder.NodeMatcher +import io.github.kakaocup.compose.node.core.BaseNode + +/** + * Base class for all child nodes within [KListNode]. + * + * The constructor is declared as `protected` so that only inheritors have the right to call this constructor. + * + * Warning! Manually creating list items is **not necessary**. + */ +open class KListItemNode> protected constructor() : BaseNode() { + + companion object { + + /** + * Method for correctly initializing the necessary parameters of [BaseNode]. + * This method allows us to keep the main constructor of the element empty, which greatly + * simplifies the description of subclass elements. + * + * @param listNode The root node of the list within which we need to interact with the list item. + * @param semanticsNode A list of key/value pairs associated with the layout node or its subtree. + * @param useUnmergedTree If true, the unmerged semantic tree will be used to work with the node. + */ + inline fun > newInstance( + listNode: KListNode, + semanticsNode: SemanticsNode, + useUnmergedTree: Boolean, + ): T { + val instance = T::class.java.getDeclaredConstructor().newInstance() + + instance.initSemantics( + semanticsProvider = listNode.getSemanticsProvider(), + nodeMatcher = NodeMatcher( + matcher = SemanticsMatcher( + description = "Semantics node id = ${semanticsNode.id}", + matcher = { it.id == semanticsNode.id }, + ), + useUnmergedTree = useUnmergedTree, + ), + parentNode = listNode, + ) + + return instance + } + + } + +} From 81601250894cdb4d19ea6a79f45925570da91c4f Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 19:06:59 +0500 Subject: [PATCH 09/20] [klistnode] Adds KNode for lists --- .../node/assertion/ListNodeAssertions.kt | 40 ++ .../compose/node/element/list/KListNode.kt | 456 ++++++++++++++++++ 2 files changed, 496 insertions(+) create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ListNodeAssertions.kt create mode 100644 compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ListNodeAssertions.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ListNodeAssertions.kt new file mode 100644 index 00000000..f1c1d5f1 --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ListNodeAssertions.kt @@ -0,0 +1,40 @@ +package io.github.kakaocup.compose.node.assertion +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.node.assertion.NodeAssertions + +/** + * Basic assertions for working with lists. + */ +interface ListNodeAssertions : NodeAssertions { + + /** + * Semantic property for the list length. + */ + val lengthSemanticsPropertyKey: SemanticsPropertyKey + + /** + * Checks that the length of the list is equal to [length]. + * + * @throws [AssertionError] if the list length is not equal to [length]. + * @throws [IllegalStateException] if the list does not provide the [lengthSemanticsPropertyKey] property. + */ + fun assertLengthEquals(length: Int) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { + assert(hasListLength(length)) + } + } + + private fun hasListLength(length: Int): SemanticsMatcher { + return SemanticsMatcher( + "The length of the list is expected to be $length, but the actual size is different" + ) { node -> + val actualLength = node.config.getOrNull(lengthSemanticsPropertyKey) + ?: error("List does not contain $lengthSemanticsPropertyKey modifier") + + actualLength == length + } + } +} \ No newline at end of file diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt new file mode 100644 index 00000000..e907a5e1 --- /dev/null +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt @@ -0,0 +1,456 @@ +package io.github.kakaocup.compose.node.element.list + +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.filter +import androidx.compose.ui.test.onChildren +import io.github.kakaocup.compose.node.action.NodeActions.ComposeBaseActionType +import io.github.kakaocup.compose.node.assertion.ListNodeAssertions +import io.github.kakaocup.compose.node.builder.NodeMatcher +import io.github.kakaocup.compose.node.builder.ViewBuilder +import io.github.kakaocup.compose.node.core.BaseNode +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.utilities.checkNotNull +import io.github.kakaocup.compose.utilities.orGlobal + +/** + * A slightly modified copy of [io.github.kakaocup.compose.node.element.lazylist.KLazyListNode]. + * + * [KListNode] is intended for testing any lists. + * + * It can be used with both lazy and non-lazy containers containing any elements. + * + * - [androidx.compose.foundation.lazy.LazyColumn]. + * - [androidx.compose.foundation.lazy.LazyRow]. + * - [androidx.compose.foundation.layout.Row] + Modifier.[androidx.compose.foundation.horizontalScroll] (or without it). + * - [androidx.compose.foundation.layout.Column] + Modifier.[androidx.compose.foundation.verticalScroll] (or without it). + * + * @param semanticsProvider ComposeRule for finding and interacting with semantic tree nodes. + * @param nodeMatcher Node matcher. + * @param parentNode Parent node (if any). + * @param useUnmergedTree If true, use the unmerged semantic tree; otherwise, use the merged tree. + * @param isScrollable If true, the [KListNode] is considered scrollable and allows scrolling by default + * when searching for individual items. If false, no scrolling will occur. By default, it is assumed that the list can + * scroll itself. + * @param itemIndexSemanticsPropertyKey Semantic property key for the list item index. + * @param lengthSemanticsPropertyKey Semantic property key for the list length. + */ +class KListNode( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>?, + val useUnmergedTree: Boolean = true, + val isScrollable: Boolean = true, + val itemIndexSemanticsPropertyKey: SemanticsPropertyKey?, + override val lengthSemanticsPropertyKey: SemanticsPropertyKey, +) : BaseNode(semanticsProvider, nodeMatcher, parentNode), + ListNodeAssertions { + + companion object { + val itemIndexPropertyErrorMessage = """ + |[itemIndexSemanticsPropertyKey] parameter is not specified for KListNode + |Configure it by using [KListNode(.., itemIndexSemanticsPropertyKey = ListItemIndexPropertyKey)]""" + .trimMargin() + } + + val rootNodeMatcher = nodeMatcher.matcher + + /** + * Constructor for creating [KListNode] using a convenient [ViewBuilder]. + * + * @param semanticsProvider ComposeRule for finding and interacting with semantic tree nodes. + * @param viewBuilderAction Lambda for building the node matcher using [ViewBuilder]. + * @param parentNode Parent node (if any). + * @param useUnmergedTree If true, use the unmerged semantic tree; otherwise, use the merged tree. + * @param isScrollable If true, the [KListNode] is considered scrollable and allows scrolling by default + * when searching for individual items. If false, no scrolling will occur. By default, it is assumed that the list can + * scroll itself. + * @param itemIndexSemanticsPropertyKey Semantic property key for the list item index. + * @param lengthSemanticsPropertyKey Semantic property key for the list length. + */ + @Suppress("detekt.LongParameterList") + constructor( + semanticsProvider: SemanticsNodeInteractionsProvider, + viewBuilderAction: ViewBuilder.() -> Unit, + parentNode: BaseNode<*>?, + useUnmergedTree: Boolean, + isScrollable: Boolean, + itemIndexSemanticsPropertyKey: SemanticsPropertyKey?, + lengthSemanticsPropertyKey: SemanticsPropertyKey, + ) : this( + semanticsProvider = semanticsProvider, + nodeMatcher = ViewBuilder().apply(viewBuilderAction).build(), + parentNode = parentNode, + useUnmergedTree = useUnmergedTree, + isScrollable = isScrollable, + itemIndexSemanticsPropertyKey = itemIndexSemanticsPropertyKey, + lengthSemanticsPropertyKey = lengthSemanticsPropertyKey, + ) + + // region Methods for checking the existence / non-existence of items in the list + + /** + * Checks that the item at the specified position is displayed. + * + * @param index Position of the item. + * + * @throws NullPointerException if [itemIndexSemanticsPropertyKey] is `null`. + * @throws [AssertionError] if the node cannot be found in the list. + */ + @OptIn(ExperimentalTestApi::class) + fun assertItemIsDisplayedAt(index: Int) { + val childMatcher = buildChildMatcher(index) + + if (isScrollable) { + delegate.perform(ComposeBaseActionType.PERFORM_SCROLL_TO_NODE) { + performScrollToNode(childMatcher.matcher) + } + } else { + getSemanticsProvider() + .onNode(rootNodeMatcher, this.useUnmergedTree) + .onChildren() + .filter(childMatcher.matcher)[childMatcher.position] + .assertIsDisplayed() + } + } + + /** + * Checks that the item at the specified position is not displayed. + * + * @param index Position of the item. + * + * @throws NullPointerException if [itemIndexSemanticsPropertyKey] is `null`. + * @throws [AssertionError] if the node can be found in the list. + */ + @OptIn(ExperimentalTestApi::class) + fun assertItemIsNotDisplayedAt(index: Int) { + val childMatcher = buildChildMatcher(index) + + if (isScrollable) { + delegate.perform(ComposeBaseActionType.PERFORM_SCROLL_TO_NODE) { + try { + performScrollToNode(childMatcher.matcher) + error("Found node that matches ${childMatcher.matcher.description}") + } catch (ex: IllegalStateException) { + // If we are at this point, it means we found a node that should not exist. + throw ex + } catch (ignored: Throwable) { + // If we are at this point -> everything is fine. + } + } + } else { + getSemanticsProvider() + .onNode(rootNodeMatcher, this.useUnmergedTree) + .onChildren() + .filter(childMatcher.matcher)[childMatcher.position] + .assertIsNotDisplayed() + } + } + + /** + * Checks that the item found using the Matcher exists in the list. + * + * @param viewBuilderAction Builder for creating the search matcher. + * + * @throws [AssertionError] if the node cannot be found in the list. + */ + @OptIn(ExperimentalTestApi::class) + fun assertItemWithIsDisplayed(viewBuilderAction: ViewBuilder.() -> Unit) { + val childMatcher = buildChildMatcher(viewBuilderAction) + + if (isScrollable) { + delegate.perform(ComposeBaseActionType.PERFORM_SCROLL_TO_NODE) { + performScrollToNode(childMatcher.matcher) + } + } else { + val childNode = this.child(viewBuilderAction) + childNode.assertIsDisplayed() + } + } + + /** + * Checks that the item found using the Matcher does not exist in the list. + * + * @param viewBuilderAction Builder for creating the search matcher. + * + * @throws [AssertionError] if the node can be found in the list. + */ + @OptIn(ExperimentalTestApi::class) + fun assertItemWithIsNotDisplayed(viewBuilderAction: ViewBuilder.() -> Unit) { + val childMatcher = buildChildMatcher(viewBuilderAction) + + if (isScrollable) { + delegate.perform(ComposeBaseActionType.PERFORM_SCROLL_TO_NODE) { + try { + performScrollToNode(childMatcher.matcher) + error("Found node that matches ${childMatcher.matcher.description}") + } catch (ex: IllegalStateException) { + // If we are at this point, it means we found a node that should not exist. + throw ex + } catch (ignored: Throwable) { + // If we are at this point -> everything is fine. + } + } + } else { + val childNode = this.child(viewBuilderAction) + childNode.assertIsNotDisplayed() + } + } + + // endregion + + // region Methods for working with items at a position + + /** + * Performs the specified actions / checks on the list item at the specified position. + * + * This method should be used if you DO NOT NEED a specific type of list item node. + * + * @param index Index of the item in the list node. + * @param needPerformScroll If true, additional scrolling will be performed to the specified position. + * By default, scrolling depends on the [isScrollable] flag. + * @param function Function for processing the item. + * + * @throws NullPointerException if [itemIndexSemanticsPropertyKey] is `null`. + */ + fun itemAt( + index: Int, + needPerformScroll: Boolean = isScrollable, + function: KListItemNode<*>.() -> Unit, + ) { + childAt>(index, needPerformScroll, function) + } + + /** + * Performs the specified actions / checks on the list item at the specified position. + * + * This method should be used if you NEED a specific type of list item node. + * + * @param T Specific type of list item. + * @param index Index of the item in the list node. + * @param needPerformScroll If true, additional scrolling will be performed to the specified position. + * By default, scrolling depends on the [isScrollable] flag. + * @param function Function for processing the item. + * + * @throws NullPointerException if [itemIndexSemanticsPropertyKey] is `null`. + */ + inline fun > childAt( + index: Int, + needPerformScroll: Boolean = isScrollable, + function: T.() -> Unit, + ) { + val childItem = getChildAt(index, needPerformScroll = needPerformScroll) + function.invoke(childItem) + } + + /** + * Performs the specified actions / checks on the list item at the first position. + * + * This method should be used if you DO NOT NEED a specific type of list item node. + * + * @param needPerformScroll If true, additional scrolling will be performed to the first position. + * By default, scrolling depends on the [isScrollable] flag. + * @param function Function for processing the item. + */ + fun firstItem( + needPerformScroll: Boolean = isScrollable, + function: KListItemNode<*>.() -> Unit, + ) { + firstChild(needPerformScroll, function) + } + + /** + * Performs the specified actions / checks on the list item at the first position. + * + * This method should be used if you NEED a specific type of list item node. + * + * @param T Specific type of list item. + * @param needPerformScroll If true, additional scrolling will be performed to the first position. + * By default, scrolling depends on the [isScrollable] flag. + * @param function Function for processing the item. + */ + inline fun > firstChild( + needPerformScroll: Boolean = isScrollable, + function: T.() -> Unit, + ) { + childAt(0, needPerformScroll, function) + } + + // endregion + + // region Methods for working with items by Matcher + + /** + * Finds a child element of the list node using [ViewBuilder] and performs [function]. + * + * This method should be used if you DO NOT NEED a specific type of list item node. + * + * @param viewBuilderAction Builder for creating the search matcher. + * @param needPerformScroll If true, additional scrolling will be performed to the element with + * the built [SemanticsMatcher]. By default, scrolling depends on the [isScrollable] flag. + * @param function Function for processing the item. + */ + fun itemWith( + viewBuilderAction: ViewBuilder.() -> Unit, + needPerformScroll: Boolean = isScrollable, + function: KListItemNode<*>.() -> Unit, + ) { + val item = getItemWith(viewBuilderAction, needPerformScroll) + function.invoke(item) + } + + /** + * Finds a child element of the list node using [ViewBuilder] and performs [function]. + * + * This method should be used if you NEED a specific type of list item node. + * + * @param T Specific type of list item. + * @param viewBuilderAction Builder for creating the search matcher. + * @param needPerformScroll If true, additional scrolling will be performed to the element with + * the built [SemanticsMatcher]. By default, scrolling depends on the [isScrollable] flag. + * @param function Function for processing the item. + */ + inline fun > childWith( + noinline viewBuilderAction: ViewBuilder.() -> Unit, + needPerformScroll: Boolean = isScrollable, + function: T.() -> Unit, + ) { + val item = getChildWith(viewBuilderAction, needPerformScroll) + function.invoke(item) + } + + // endregion + + // region Getters for list items + + /** + * Returns a child element of the list node found using [ViewBuilder]. + * + * This method should be used if you DO NOT NEED a specific type of list item node. + * + * @param viewBuilderAction Builder for creating the search matcher. + * @param needPerformScroll If true, additional scrolling will be performed to the element with + * the built [SemanticsMatcher]. By default, scrolling depends on the [isScrollable] flag. + */ + fun getItemWith( + viewBuilderAction: ViewBuilder.() -> Unit, + needPerformScroll: Boolean = isScrollable, + ): KListItemNode<*> = + getChildWith(viewBuilderAction, needPerformScroll) + + /** + * Returns a child element of the list node found using [ViewBuilder]. + * + * This method should be used if you NEED a specific type of list item node. + * + * @param T Specific type of list item. + * @param viewBuilderAction Builder for creating the search matcher. + * @param needPerformScroll If true, additional scrolling will be performed to the element with + * the built [SemanticsMatcher]. By default, scrolling depends on the [isScrollable] flag. + */ + @OptIn(ExperimentalTestApi::class) + inline fun > getChildWith( + noinline viewBuilderAction: ViewBuilder.() -> Unit, + needPerformScroll: Boolean = isScrollable, + ): T { + val childMatcher = buildChildMatcher(viewBuilderAction) + + if (needPerformScroll) { + performScrollToNode(childMatcher.matcher) + } + + val semanticsNode = getSemanticsProvider() + .onNode(rootNodeMatcher, this.useUnmergedTree) + .onChildren() + .filter(childMatcher.matcher)[childMatcher.position] + .fetchSemanticsNode() + + return createListItemNode(semanticsNode) + } + + /** + * Returns a child element of the list node at the specified position. + * Depending on the [needPerformScroll] parameter, additional scrolling may be performed to the element at + * the specified position. + * + * This method should be used if you DO NOT NEED a specific type of list item node. + * + * @param index Index of the item in the list node. + * @param needPerformScroll If true, we will try to scroll to the specified position. + * + * @throws NullPointerException if [needPerformScroll] is true and [itemIndexSemanticsPropertyKey] is `null`. + */ + fun getItemAt(index: Int, needPerformScroll: Boolean = isScrollable): KListItemNode<*> = + getChildAt(index, needPerformScroll) + + /** + * Returns a child element of the list node at the specified position. + * Depending on the [needPerformScroll] parameter, additional scrolling may be performed to the element at + * the specified position. + * + * This method should be used if you NEED a specific type of list item node. + * + * @param T Specific type of element. + * @param index Index of the item in the list node. + * @param needPerformScroll If true, we will try to scroll to the specified position. + * + * @throws NullPointerException if [needPerformScroll] is true and [itemIndexSemanticsPropertyKey] is `null`. + */ + @OptIn(ExperimentalTestApi::class) + inline fun > getChildAt(index: Int, needPerformScroll: Boolean = isScrollable): T { + val childMatcher = buildChildMatcher(index) + + if (needPerformScroll) { + performScrollToNode(childMatcher.matcher) + } + + // Warning! + // Within lazy collections, `filterToOne` cannot be used on child nodes. + // In Compose version 1.5.5, the semantic tree of lazy collections MAY CONTAIN DUPLICATES. + val semanticsNode = getSemanticsProvider() + .onNode(rootNodeMatcher, this.useUnmergedTree) + .onChildren() + .filter(childMatcher.matcher)[childMatcher.position] + .fetchSemanticsNode() + + return createListItemNode(semanticsNode) + } + + // endregion + + inline fun > createListItemNode(semanticsNode: SemanticsNode): T { + return KListItemNode.newInstance( + listNode = this, + semanticsNode = semanticsNode, + useUnmergedTree = useUnmergedTree, + ) + } + + fun buildChildMatcher(viewBuilderAction: ViewBuilder.() -> Unit): NodeMatcher { + return ViewBuilder() + .apply { + this.useUnmergedTree = this@KListNode.useUnmergedTree + } + .apply(viewBuilderAction) + .build() + } + + fun buildChildMatcher(index: Int): NodeMatcher { + requireNotNull(itemIndexSemanticsPropertyKey) { itemIndexPropertyErrorMessage } + + return ViewBuilder() + .apply { + this.useUnmergedTree = this@KListNode.useUnmergedTree + addSemanticsMatcher( + SemanticsMatcher.expectValue(this@KListNode.itemIndexSemanticsPropertyKey, index) + ) + } + .build() + } + +} From 517a78cca05c5db1e2d9cabb41f13bf8a774132d Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 19:10:52 +0500 Subject: [PATCH 10/20] [klistnode] Adds semantics keys for list item index and list length for sample app --- .../compose/sample/semantics/ListItemIndex.kt | 12 ++++++++++++ .../kakaocup/compose/sample/semantics/ListLength.kt | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 sample/src/main/java/io/github/kakaocup/compose/sample/semantics/ListItemIndex.kt create mode 100644 sample/src/main/java/io/github/kakaocup/compose/sample/semantics/ListLength.kt diff --git a/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/ListItemIndex.kt b/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/ListItemIndex.kt new file mode 100644 index 00000000..c7b55a71 --- /dev/null +++ b/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/ListItemIndex.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 ListItemIndexSemanticKey = SemanticsPropertyKey("ListItemIndex") +var SemanticsPropertyReceiver.listItemIndex by ListItemIndexSemanticKey +fun Modifier.listItemIndexSemantic(index: Int): Modifier { + return semantics { listItemIndex = index } +} diff --git a/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/ListLength.kt b/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/ListLength.kt new file mode 100644 index 00000000..3a56f216 --- /dev/null +++ b/sample/src/main/java/io/github/kakaocup/compose/sample/semantics/ListLength.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 ListLengthSemanticKey = SemanticsPropertyKey("ListLength") +var SemanticsPropertyReceiver.listLength by ListLengthSemanticKey +fun Modifier.listLengthSemantic(length: Int): Modifier { + return semantics { listLength = length } +} From ae54a95aab3905e59307751c486d3fa604f0b55d Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 20:01:02 +0500 Subject: [PATCH 11/20] [klistnode] Fixes function names collision in BaseNode --- .../kakaocup/compose/node/core/BaseNode.kt | 18 +++++++++--------- .../compose/node/element/ComposeScreen.kt | 3 +-- .../node/element/lazylist/KLazyListNode.kt | 10 ++++------ .../compose/node/element/list/KListItemNode.kt | 2 +- .../compose/node/element/list/KListNode.kt | 10 ++++------ 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index 0b2e92ef..8a4530b1 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -51,10 +51,10 @@ abstract class BaseNode> constructor( nodeProvider = NodeProvider( nodeMatcher = NodeMatcher( matcher = combineSemanticMatchers(), - position = getNodeMatcher().position, - useUnmergedTree = getNodeMatcher().useUnmergedTree + position = requireNodeMatcher().position, + useUnmergedTree = requireNodeMatcher().useUnmergedTree ), - semanticsProvider = getSemanticsProvider() + semanticsProvider = requireSemanticsProvider() ), parentDelegate = parentNode?.delegate ) @@ -66,7 +66,7 @@ abstract class BaseNode> constructor( NodeMatcher::class.java, BaseNode::class.java, ).newInstance( - semanticsProvider, + requireSemanticsProvider(), ViewBuilder().apply(function).build(), this, ) @@ -76,16 +76,16 @@ abstract class BaseNode> constructor( * Allowed getter for [nodeMatcher]. * Any [NodeMatcher] must be initialized before use. */ - fun getNodeMatcher(): NodeMatcher { + fun requireNodeMatcher(): NodeMatcher { return this.nodeMatcher ?: throw KakaoComposeException("NodeMatcher is null: Provide via constructor or use `initSemantics` method`") } /** - * Allowed getter for [semanticsProvider]. + * Allowed getter for [requireSemanticsProvider]. * Any [SemanticsNodeInteractionsProvider] must be initialized before use. */ - fun getSemanticsProvider(): SemanticsNodeInteractionsProvider { + fun requireSemanticsProvider(): SemanticsNodeInteractionsProvider { return this.semanticsProvider.orGlobal().checkNotNull() } @@ -111,10 +111,10 @@ abstract class BaseNode> constructor( var parent = this.parentNode while (parent != null) { - semanticsMatcherList.add(hasAnyAncestor(parent.getNodeMatcher().matcher)) + semanticsMatcherList.add(hasAnyAncestor(parent.requireNodeMatcher().matcher)) parent = parent.parentNode } - semanticsMatcherList.add(this.getNodeMatcher().matcher) + semanticsMatcherList.add(this.requireNodeMatcher().matcher) return semanticsMatcherList.reduce { finalMatcher, matcher -> finalMatcher and matcher } } diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt index 167e91cf..8a3361d4 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt @@ -2,7 +2,6 @@ package io.github.kakaocup.compose.node.element import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.node.core.ComposeMarker import io.github.kakaocup.compose.node.core.BaseNode import io.github.kakaocup.compose.node.builder.NodeMatcher @@ -34,7 +33,7 @@ open class ComposeScreen> : BaseNode { ) : super(semanticsProvider, nodeMatcher) fun onNode(viewBuilderAction: ViewBuilder.() -> Unit) = KNode( - getSemanticsProvider(), + requireSemanticsProvider(), viewBuilderAction, ) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt index 88b42a32..2d7b25e5 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt @@ -7,8 +7,6 @@ import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.filter import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.onChildren -import io.github.kakaocup.compose.KakaoCompose -import io.github.kakaocup.compose.exception.KakaoComposeException import io.github.kakaocup.compose.node.assertion.LazyListNodeAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder @@ -69,7 +67,7 @@ class KLazyListNode( }.provideItem performScrollToIndex(position) - val semanticsNode = getSemanticsProvider() + val semanticsNode = requireSemanticsProvider() .onNode(semanticsMatcher) .onChildren() .filterToOne(positionMatcher(position)) @@ -77,7 +75,7 @@ class KLazyListNode( function(provideItem( semanticsNode, - getSemanticsProvider() + requireSemanticsProvider() ) as T) } @@ -98,7 +96,7 @@ class KLazyListNode( performScrollToNode(nodeMatcher.matcher) - val semanticsNode = getSemanticsProvider() + val semanticsNode = requireSemanticsProvider() .onNode(semanticsMatcher) .onChildren() .filter(nodeMatcher.matcher)[nodeMatcher.position] @@ -106,7 +104,7 @@ class KLazyListNode( return provideItem( semanticsNode, - getSemanticsProvider() + requireSemanticsProvider() ) as T } diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt index 33b250cc..59eb766c 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt @@ -33,7 +33,7 @@ open class KListItemNode> protected constructor() : Bas val instance = T::class.java.getDeclaredConstructor().newInstance() instance.initSemantics( - semanticsProvider = listNode.getSemanticsProvider(), + semanticsProvider = listNode.requireSemanticsProvider(), nodeMatcher = NodeMatcher( matcher = SemanticsMatcher( description = "Semantics node id = ${semanticsNode.id}", diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt index e907a5e1..43f236f2 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt @@ -15,8 +15,6 @@ import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder import io.github.kakaocup.compose.node.core.BaseNode import io.github.kakaocup.compose.node.element.KNode -import io.github.kakaocup.compose.utilities.checkNotNull -import io.github.kakaocup.compose.utilities.orGlobal /** * A slightly modified copy of [io.github.kakaocup.compose.node.element.lazylist.KLazyListNode]. @@ -111,7 +109,7 @@ class KListNode( performScrollToNode(childMatcher.matcher) } } else { - getSemanticsProvider() + requireSemanticsProvider() .onNode(rootNodeMatcher, this.useUnmergedTree) .onChildren() .filter(childMatcher.matcher)[childMatcher.position] @@ -144,7 +142,7 @@ class KListNode( } } } else { - getSemanticsProvider() + requireSemanticsProvider() .onNode(rootNodeMatcher, this.useUnmergedTree) .onChildren() .filter(childMatcher.matcher)[childMatcher.position] @@ -364,7 +362,7 @@ class KListNode( performScrollToNode(childMatcher.matcher) } - val semanticsNode = getSemanticsProvider() + val semanticsNode = requireSemanticsProvider() .onNode(rootNodeMatcher, this.useUnmergedTree) .onChildren() .filter(childMatcher.matcher)[childMatcher.position] @@ -412,7 +410,7 @@ class KListNode( // Warning! // Within lazy collections, `filterToOne` cannot be used on child nodes. // In Compose version 1.5.5, the semantic tree of lazy collections MAY CONTAIN DUPLICATES. - val semanticsNode = getSemanticsProvider() + val semanticsNode = requireSemanticsProvider() .onNode(rootNodeMatcher, this.useUnmergedTree) .onChildren() .filter(childMatcher.matcher)[childMatcher.position] From 00911d2e236822d7b0d1e132a5f55d2c65bd7c57 Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 22:33:34 +0500 Subject: [PATCH 12/20] [klistnode] Changes packaging options for sample app to enable Layout Inspector --- sample/build.gradle.kts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 94c63ac0..7b46a77a 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -36,7 +36,12 @@ android { kotlinCompilerExtensionVersion = libs.versions.composeCompilerVersion.get() } - packaging.resources.excludes.add("META-INF/*") + packaging { + resources { + pickFirsts += "META-INF/androidx.compose.*.version" + excludes += "META-INF/*" + } + } kotlin { jvmToolchain(17) From b5e833305ab5ff79f4daa22c80011d1f6fac4dca Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 22:49:57 +0500 Subject: [PATCH 13/20] [klistnode] Takes into account the value of a global variable Kakao.Overrides.useUnmergedTree when passing a useUnmergedTree flag to the constructor of KListNode. --- .../io/github/kakaocup/compose/node/element/list/KListNode.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt index 43f236f2..8560fd96 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.filter import androidx.compose.ui.test.onChildren +import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.node.action.NodeActions.ComposeBaseActionType import io.github.kakaocup.compose.node.assertion.ListNodeAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher @@ -42,7 +43,7 @@ class KListNode( semanticsProvider: SemanticsNodeInteractionsProvider, nodeMatcher: NodeMatcher, parentNode: BaseNode<*>?, - val useUnmergedTree: Boolean = true, + val useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false, val isScrollable: Boolean = true, val itemIndexSemanticsPropertyKey: SemanticsPropertyKey?, override val lengthSemanticsPropertyKey: SemanticsPropertyKey, From b60f80e970abee7fadde72394135a6f2afc306f2 Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 22:50:30 +0500 Subject: [PATCH 14/20] [klistnode] Overrides performScrollToIndex action to a more reliable solution --- .../compose/node/element/list/KListNode.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt index 8560fd96..c16a0894 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt @@ -91,6 +91,28 @@ class KListNode( lengthSemanticsPropertyKey = lengthSemanticsPropertyKey, ) + // region Overriding base node actions + + /** + * Performs scroll to position with [index]. + * + * We have to override this action from [io.github.kakaocup.compose.node.action.NodeActions], because not every + * scrollable container has [androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex] action. + * Using [androidx.compose.ui.semantics.SemanticsActions.ScrollBy] action is a more reliable solution + * with child Matcher on item index. + * + * @throws NullPointerException if [itemIndexSemanticsPropertyKey] is `null`. + * @throws [IllegalStateException] if this node marked as not scrollable (see [isScrollable]). + * @throws [AssertionError] if node doesn't have [androidx.compose.ui.semantics.SemanticsActions.ScrollBy] action. + */ + @OptIn(ExperimentalTestApi::class) + override fun performScrollToIndex(index: Int) { + check(isScrollable) + getItemAt(0) + } + + // endregion + // region Methods for checking the existence / non-existence of items in the list /** From cef68cb0ba16b4981503b8d5508dfdae7e8c2049 Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 22:52:17 +0500 Subject: [PATCH 15/20] [klistnode] Adds addtional factory methods for KListNode --- .../compose/node/element/list/KListNode.kt | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt index c16a0894..694eac7c 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt @@ -475,3 +475,70 @@ class KListNode( } } + +// region Additional factory methods + +/** + * Simplified builder for creating [KListNode]. + * + * @param useUnmergedTree If true, the unmerged semantic tree is used; otherwise, the merged tree is used. + * @param isScrollable If true, the [KListNode] is considered scrollable and allows scrolling by default + * when searching for individual items. If false, no scrolling will occur. By default, it is assumed that the list can + * scroll itself. + * @param itemIndexSemanticsPropertyKey Semantic property key for the list item index. + * @param lengthSemanticsPropertyKey Semantic property key for the list length. + * @param viewBuilderAction Lambda for building the node matcher using [ViewBuilder]. + */ +fun BaseNode<*>.KListNode( + useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false, + isScrollable: Boolean = true, + itemIndexSemanticsPropertyKey: SemanticsPropertyKey? = null, + lengthSemanticsPropertyKey: SemanticsPropertyKey, + viewBuilderAction: ViewBuilder.() -> Unit, +): KListNode { + val nodeMatcher = ViewBuilder() + .apply { + this.useUnmergedTree = useUnmergedTree + } + .apply(viewBuilderAction) + .build() + + return KListNode( + semanticsProvider = requireSemanticsProvider(), + nodeMatcher = nodeMatcher, + parentNode = this, + useUnmergedTree = useUnmergedTree, + isScrollable = isScrollable, + itemIndexSemanticsPropertyKey = itemIndexSemanticsPropertyKey, + lengthSemanticsPropertyKey = lengthSemanticsPropertyKey, + ) +} + +/** + * Simplified builder for creating [KListNode] with tesgTag. + * + * @param testTag Tag for searching the node. + * @param useUnmergedTree If true, the unmerged semantic tree is used; otherwise, the merged tree is used. + * @param isScrollable If true, the [KListNode] is considered scrollable and allows scrolling by default + * when searching for individual items. If false, no scrolling will occur. By default, it is assumed that the list can + * scroll itself. + * @param itemIndexSemanticsPropertyKey Semantic property key for the list item index. + * @param lengthSemanticsPropertyKey Semantic property key for the list length. + */ +fun BaseNode<*>.KListNode( + testTag: String, + useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false, + isScrollable: Boolean = true, + itemIndexSemanticsPropertyKey: SemanticsPropertyKey? = null, + lengthSemanticsPropertyKey: SemanticsPropertyKey, +): KListNode = KListNode( + useUnmergedTree = useUnmergedTree, + isScrollable = isScrollable, + itemIndexSemanticsPropertyKey = itemIndexSemanticsPropertyKey, + lengthSemanticsPropertyKey = lengthSemanticsPropertyKey, +) { + hasTestTag(testTag) + this.useUnmergedTree = useUnmergedTree +} + +// endregion From c19cfb75f234d810f2dc17042a79f48e4e2e4f38 Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 22:54:19 +0500 Subject: [PATCH 16/20] [klistnode] Adds tests for KListNode --- .../kakaocup/compose/SampleNoActivityTest.kt | 19 ++ .../kakaocup/compose/node/KAppListNode.kt | 18 ++ .../list/NotScrollableListComposeScreen.kt | 31 +++ .../list/ScrollableLazyListComposeScreen.kt | 23 ++ .../list/ScrollableListComposeScreen.kt | 23 ++ .../test/list/NotScrollableListTest.kt | 63 +++++ .../test/list/ScrollableLazyListTest.kt | 68 ++++++ .../compose/test/list/ScrollableListTest.kt | 70 ++++++ .../compose/sample/NotScrollableListScreen.kt | 231 ++++++++++++++++++ .../sample/ScrollableLazyListScreen.kt | 129 ++++++++++ .../compose/sample/ScrollableListScreen.kt | 129 ++++++++++ 11 files changed, 804 insertions(+) create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/SampleNoActivityTest.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppListNode.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/NotScrollableListComposeScreen.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/ScrollableLazyListComposeScreen.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/ScrollableListComposeScreen.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/test/list/NotScrollableListTest.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/test/list/ScrollableLazyListTest.kt create mode 100644 sample/src/androidTest/java/io/github/kakaocup/compose/test/list/ScrollableListTest.kt create mode 100644 sample/src/main/java/io/github/kakaocup/compose/sample/NotScrollableListScreen.kt create mode 100644 sample/src/main/java/io/github/kakaocup/compose/sample/ScrollableLazyListScreen.kt create mode 100644 sample/src/main/java/io/github/kakaocup/compose/sample/ScrollableListScreen.kt diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/SampleNoActivityTest.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/SampleNoActivityTest.kt new file mode 100644 index 00000000..6f8574ed --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/SampleNoActivityTest.kt @@ -0,0 +1,19 @@ +package io.github.kakaocup.compose + +import androidx.compose.ui.test.junit4.createComposeRule +import io.github.kakaocup.compose.rule.KakaoComposeTestRule +import org.junit.Rule + +abstract class SampleNoActivityTest { + + init { + KakaoCompose.Override.useUnmergedTree = true + } + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val kakaoComposeTestRule = KakaoComposeTestRule(composeTestRule) + +} diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppListNode.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppListNode.kt new file mode 100644 index 00000000..bfed9d83 --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/node/KAppListNode.kt @@ -0,0 +1,18 @@ +package io.github.kakaocup.compose.node + +import io.github.kakaocup.compose.node.core.BaseNode +import io.github.kakaocup.compose.node.element.list.KListNode +import io.github.kakaocup.compose.sample.semantics.ListItemIndexSemanticKey +import io.github.kakaocup.compose.sample.semantics.ListLengthSemanticKey + +fun BaseNode<*>.KAppListNode( + testTag: String, + isScrollable: Boolean = true, +) = KListNode( + useUnmergedTree = true, + itemIndexSemanticsPropertyKey = ListItemIndexSemanticKey, + lengthSemanticsPropertyKey = ListLengthSemanticKey, + isScrollable = isScrollable, +) { + hasTestTag(testTag) +} diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/NotScrollableListComposeScreen.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/NotScrollableListComposeScreen.kt new file mode 100644 index 00000000..9449ee27 --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/NotScrollableListComposeScreen.kt @@ -0,0 +1,31 @@ +package io.github.kakaocup.compose.screen.list + +import io.github.kakaocup.compose.node.KAppListNode +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.list.KListItemNode + +class NotScrollableListComposeScreen : ComposeScreen() { + + val notScrollableListNode = KAppListNode( + testTag = "tabbar_container", + isScrollable = false, + ) + + class TabBarListItemNode : KListItemNode() { + + val text: KNode by lazy { + child { + hasTestTag("tabbar_item_text") + } + } + + val icon: KNode by lazy { + child { + hasTestTag("tabbar_item_icon") + } + } + + } + +} \ No newline at end of file diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/ScrollableLazyListComposeScreen.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/ScrollableLazyListComposeScreen.kt new file mode 100644 index 00000000..c0abfb05 --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/ScrollableLazyListComposeScreen.kt @@ -0,0 +1,23 @@ +package io.github.kakaocup.compose.screen.list + +import io.github.kakaocup.compose.node.KAppListNode +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.list.KListItemNode + +class ScrollableLazyListComposeScreen : ComposeScreen() { + + val lazyList = KAppListNode("LazyList") + + val pullToRefresh = KNode() { hasTestTag("PullToRefresh") } + + class ListHeaderNode : KListItemNode() { + + val title: KNode by lazy { + child { + hasTestTag("LazyListHeaderTitle") + } + } + } + +} diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/ScrollableListComposeScreen.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/ScrollableListComposeScreen.kt new file mode 100644 index 00000000..e3b62067 --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/screen/list/ScrollableListComposeScreen.kt @@ -0,0 +1,23 @@ +package io.github.kakaocup.compose.screen.list + +import io.github.kakaocup.compose.node.KAppListNode +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.list.KListItemNode + +class ScrollableListComposeScreen : ComposeScreen() { + + val scrollableList = KAppListNode("ScrollableColumn") + + val pullToRefresh = KNode() { hasTestTag("PullToRefresh") } + + class ListHeaderNode : KListItemNode() { + + val title: KNode by lazy { + child { + hasTestTag("ListHeaderTitle") + } + } + } + +} diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/NotScrollableListTest.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/NotScrollableListTest.kt new file mode 100644 index 00000000..4571dd95 --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/NotScrollableListTest.kt @@ -0,0 +1,63 @@ +package io.github.kakaocup.compose.test.list + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Email +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.junit4.createComposeRule +import io.github.kakaocup.compose.KakaoCompose +import io.github.kakaocup.compose.SampleNoActivityTest +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.rule.KakaoComposeTestRule +import io.github.kakaocup.compose.sample.NotScrollableListScreen +import io.github.kakaocup.compose.sample.semantics.ImageContentSemanticKey +import io.github.kakaocup.compose.screen.list.NotScrollableListComposeScreen +import org.junit.Rule +import org.junit.Test + +class NotScrollableListTest : SampleNoActivityTest() { + + @Test + fun test() { + composeTestRule.setContent { + MaterialTheme { + NotScrollableListScreen() + } + } + + onComposeScreen() { + notScrollableListNode { + assertIsDisplayed() + + assertLengthEquals(5) + + itemAt(1) { + performClick() + assertIsSelected() + } + + childAt(2) { + text { + assertTextEquals("Responses") + } + icon { + assert( + SemanticsMatcher.expectValue(ImageContentSemanticKey, Icons.Outlined.Email) + ) + } + } + + itemWith({ + hasAnyDescendant( + androidx.compose.ui.test.hasText("Profile") + ) + }) { + assertIsNotSelected() + performClick() + assertIsSelected() + } + } + } + } + +} \ No newline at end of file diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/ScrollableLazyListTest.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/ScrollableLazyListTest.kt new file mode 100644 index 00000000..b78728cf --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/ScrollableLazyListTest.kt @@ -0,0 +1,68 @@ +package io.github.kakaocup.compose.test.list + +import androidx.compose.material.MaterialTheme +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.swipeDown +import io.github.kakaocup.compose.SampleNoActivityTest +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.sample.ScrollableLazyListScreen +import io.github.kakaocup.compose.screen.list.ScrollableLazyListComposeScreen +import org.junit.Test + +class ScrollableLazyListTest : SampleNoActivityTest() { + + @OptIn(ExperimentalTestApi::class) + @Test + fun test() { + composeTestRule.setContent { + MaterialTheme { + ScrollableLazyListScreen() + } + } + + onComposeScreen() { + lazyList.assertLengthEquals(0) + + pullToRefresh.performTouchInput { + swipeDown(startY = 200f) + } + + lazyList { + assertLengthEquals(33) + firstChild { + title.assertTextEquals("Items from 1 to 10") + } + + itemWith( + { hasText("Item 1") } + ) { + assertTextEquals("Item 1") + } + + itemAt(10) { + assertTextEquals("Item 10") + } + childWith( + { hasAnyDescendant(androidx.compose.ui.test.hasText("Items from 21 to 30")) } + ) { + title.assertTextEquals("Items from 21 to 30") + } + itemAt(23) { + assertTextEquals("Item 21") + } + itemAt(32) { + assertTextEquals("Item 30") + } + } + + lazyList.performScrollToIndex(0) + + pullToRefresh.performTouchInput { + swipeDown(startY = 200f) + } + lazyList.assertLengthEquals(34) + + } + } + +} \ No newline at end of file diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/ScrollableListTest.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/ScrollableListTest.kt new file mode 100644 index 00000000..c8b591db --- /dev/null +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/test/list/ScrollableListTest.kt @@ -0,0 +1,70 @@ +package io.github.kakaocup.compose.test.list + +import androidx.compose.material.MaterialTheme +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.swipeDown +import io.github.kakaocup.compose.SampleNoActivityTest +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.sample.ScrollableListScreen +import io.github.kakaocup.compose.screen.LazyListHeaderNode +import io.github.kakaocup.compose.screen.LazyListItemNode +import io.github.kakaocup.compose.screen.list.ScrollableListComposeScreen +import org.junit.Test + +class ScrollableListTest : SampleNoActivityTest() { + + @OptIn(ExperimentalTestApi::class) + @Test + fun test() { + composeTestRule.setContent { + MaterialTheme { + ScrollableListScreen() + } + } + + onComposeScreen() { + scrollableList.assertLengthEquals(0) + + pullToRefresh.performTouchInput { + swipeDown(startY = 200f) + } + + scrollableList { + assertLengthEquals(33) + firstChild { + title.assertTextEquals("Items from 1 to 10") + } + + itemWith( + { hasText("Item 1") } + ) { + assertTextEquals("Item 1") + } + + itemAt(10) { + assertTextEquals("Item 10") + } + childWith( + { hasAnyDescendant(androidx.compose.ui.test.hasText("Items from 21 to 30")) } + ) { + title.assertTextEquals("Items from 21 to 30") + } + itemAt(23) { + assertTextEquals("Item 21") + } + itemAt(32) { + assertTextEquals("Item 30") + } + } + + scrollableList.performScrollToIndex(0) + + pullToRefresh.performTouchInput { + swipeDown(startY = 200f) + } + scrollableList.assertLengthEquals(34) + + } + } + +} \ No newline at end of file diff --git a/sample/src/main/java/io/github/kakaocup/compose/sample/NotScrollableListScreen.kt b/sample/src/main/java/io/github/kakaocup/compose/sample/NotScrollableListScreen.kt new file mode 100644 index 00000000..7b4e06e4 --- /dev/null +++ b/sample/src/main/java/io/github/kakaocup/compose/sample/NotScrollableListScreen.kt @@ -0,0 +1,231 @@ +package io.github.kakaocup.compose.sample + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.selectable +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.Email +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.outlined.Email +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.Send +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.style.TextOverflow +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.listItemIndexSemantic +import io.github.kakaocup.compose.sample.semantics.listLength + +/** + * Sample with not scrollable list. + */ +@Composable +fun NotScrollableListScreen() { + Box( + modifier = Modifier + .fillMaxSize() + ) { + var selectedTabId by remember { + mutableStateOf(TabBarItem.Search.id) + } + + CustomTabBar( + selectedTabId = selectedTabId, + onSelect = { selectedTabId = it }, + modifier = Modifier + .align(Alignment.BottomStart) + ) + } +} + +/** + * Example of not scrollable list. + */ +@Composable +private fun CustomTabBar( + selectedTabId: String, + onSelect: (id: String) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .heightIn(min = 49.dp) + .background(Color.White) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(Color.Gray) + ) + + // Warning! + // There is no `Modifier.horizontalScroll`, so there is no `AccessibilityAction.ScrollBy` in this container. + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + .semantics { + testTag = "tabbar_container" + listLength = TabBarItem.entries.size + }, + ) { + TabBarItem.entries.forEachIndexed { index, tabBarItem -> + TabBarItemContent( + item = tabBarItem, + selectedTabId = selectedTabId, + onSelect = onSelect, + modifier = Modifier + .weight(1f) + .listItemIndexSemantic(index) + ) + } + } + } +} + +@Composable +private fun TabBarItemContent( + item: TabBarItem, + selectedTabId: String, + onSelect: (id: String) -> Unit, + modifier: Modifier = Modifier, +) { + val isSelected = item.id == selectedTabId + + Box( + modifier = modifier + .selectable( + selected = isSelected, + role = Role.Tab, + interactionSource = remember { + MutableInteractionSource() + }, + indication = null, + onClick = { onSelect.invoke(item.id) } + ), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(top = 5.dp, bottom = 4.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + val iconContent = if (isSelected) item.selectedIcon else item.unselectedIcon + Icon( + imageVector = iconContent, + contentDescription = null, + modifier = Modifier + .testTag("tabbar_item_icon") + .imageContentSemantic(iconContent) + ) + + if (item.counter != null) { + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "${item.counter}", + color = Color.White, + modifier = Modifier + .background(Color.Red) + ) + } + } + Spacer(modifier = Modifier.height(3.dp)) + + Text( + text = item.text, + style = MaterialTheme.typography.button, + color = if (isSelected) Color.Black else Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .testTag("tabbar_item_text"), + ) + } + } +} + +private enum class TabBarItem( + val id: String, + val text: String, + val selectedIcon: ImageVector, + val unselectedIcon: ImageVector, + val counter: Int? = null, +) { + Search( + id = "0", + text = "Search", + selectedIcon = Icons.Filled.Search, + unselectedIcon = Icons.Outlined.Search, + ), + Favorites( + id = "1", + text = "Favorites", + selectedIcon = Icons.Filled.Favorite, + unselectedIcon = Icons.Outlined.FavoriteBorder, + counter = 100, + ), + Responses( + id = "2", + text = "Responses", + selectedIcon = Icons.Filled.Email, + unselectedIcon = Icons.Outlined.Email, + ), + Chats( + id = "3", + text = "Chats", + selectedIcon = Icons.Filled.Send, + unselectedIcon = Icons.Outlined.Send, + counter = 9, + ), + Profile( + id = "4", + text = "Profile", + selectedIcon = Icons.Filled.Person, + unselectedIcon = Icons.Outlined.Person, + ), +} + +@Preview +@Composable +private fun PreviewNotScrollableScreen() { + MaterialTheme { + NotScrollableListScreen() + } +} diff --git a/sample/src/main/java/io/github/kakaocup/compose/sample/ScrollableLazyListScreen.kt b/sample/src/main/java/io/github/kakaocup/compose/sample/ScrollableLazyListScreen.kt new file mode 100644 index 00000000..5ba29585 --- /dev/null +++ b/sample/src/main/java/io/github/kakaocup/compose/sample/ScrollableLazyListScreen.kt @@ -0,0 +1,129 @@ +package io.github.kakaocup.compose.sample + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import io.github.kakaocup.compose.sample.semantics.listItemIndexSemantic +import io.github.kakaocup.compose.sample.semantics.listLengthSemantic + +/** + * Sample with scrollable lazy list. + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ScrollableLazyListScreen() { + var items by remember { mutableStateOf(listOf()) } + var isRefreshing by remember { mutableStateOf(false) } + + Scaffold { scaffoldPadding -> + val pullRefreshState = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { + if (items.isEmpty()) { + items = getItems() + } else { + items += ScrollableLazyListItem.Item("Item ${items.size + 1}") + } + isRefreshing = false + } + ) + + Box( + modifier = Modifier + .padding(scaffoldPadding) + .testTag("PullToRefresh") + .pullRefresh(pullRefreshState) + ) { + LazyColumn( + Modifier + .fillMaxSize() + .testTag("LazyList") + .listLengthSemantic(items.size) + ) { + itemsIndexed(items) { index, item -> + when (item) { + is ScrollableLazyListItem.Header -> ListItemHeader(item, Modifier.listItemIndexSemantic(index)) + is ScrollableLazyListItem.Item -> ListItemCell(item, Modifier.listItemIndexSemantic(index)) + } + } + } + PullRefreshIndicator( + state = pullRefreshState, + refreshing = isRefreshing, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + } +} + + + +@Composable +private fun ListItemHeader(item: ScrollableLazyListItem.Header, modifier: Modifier = Modifier) { + Box( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color.Green) + .then(modifier) + ) { + Text( + item.title, + Modifier + .padding(8.dp) + .testTag("LazyListHeaderTitle") + ) + } +} + +@Composable +private fun ListItemCell(item: ScrollableLazyListItem.Item, modifier: Modifier = Modifier) { + Text( + item.title, + Modifier + .fillMaxWidth() + .padding(16.dp) + .testTag("LazyListItemTitle") + .then(modifier) + ) +} + +private fun getItems(): List { + val result = mutableListOf() + + repeat(30) { index -> + if (index % 10 == 0) { + result += ScrollableLazyListItem.Header(title = "Items from ${index + 1} to ${index + 10}") + } + result += ScrollableLazyListItem.Item("Item ${index + 1}") + } + + return result +} + +private sealed class ScrollableLazyListItem { + data class Header(val title: String) : ScrollableLazyListItem() + data class Item(val title: String) : ScrollableLazyListItem() +} diff --git a/sample/src/main/java/io/github/kakaocup/compose/sample/ScrollableListScreen.kt b/sample/src/main/java/io/github/kakaocup/compose/sample/ScrollableListScreen.kt new file mode 100644 index 00000000..52aa4ce0 --- /dev/null +++ b/sample/src/main/java/io/github/kakaocup/compose/sample/ScrollableListScreen.kt @@ -0,0 +1,129 @@ +package io.github.kakaocup.compose.sample + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import io.github.kakaocup.compose.sample.semantics.listItemIndexSemantic +import io.github.kakaocup.compose.sample.semantics.listLengthSemantic + +/** + * Copy of [ScrollableLazyListScreen] but with not lazy [androidx.compose.foundation.layout.Column]. + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ScrollableListScreen() { + var items by remember { mutableStateOf(listOf()) } + var isRefreshing by remember { mutableStateOf(false) } + + Scaffold { scaffoldPadding -> + val pullRefreshState = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { + if (items.isEmpty()) { + items = getItems() + } else { + items += ScrollableListItem.Item("Item ${items.size + 1}") + } + isRefreshing = false + } + ) + + Box( + modifier = Modifier + .padding(scaffoldPadding) + .testTag("PullToRefresh") + .pullRefresh(pullRefreshState) + ) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .testTag("ScrollableColumn") + .listLengthSemantic(items.size) + ) { + items.forEachIndexed { index, item -> + when (item) { + is ScrollableListItem.Header -> ListItemHeader(item, Modifier.listItemIndexSemantic(index)) + is ScrollableListItem.Item -> ListItemCell(item, Modifier.listItemIndexSemantic(index)) + } + } + } + PullRefreshIndicator( + state = pullRefreshState, + refreshing = isRefreshing, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + } +} + +@Composable +private fun ListItemHeader(item: ScrollableListItem.Header, modifier: Modifier = Modifier) { + Box( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color.Green) + .then(modifier) + ) { + Text( + item.title, + Modifier + .padding(8.dp) + .testTag("ListHeaderTitle") + ) + } +} + +@Composable +private fun ListItemCell(item: ScrollableListItem.Item, modifier: Modifier = Modifier) { + Text( + item.title, + Modifier + .fillMaxWidth() + .padding(16.dp) + .testTag("ListItemTitle") + .then(modifier) + ) +} + +private fun getItems(): List { + val result = mutableListOf() + + repeat(30) { index -> + if (index % 10 == 0) { + result += ScrollableListItem.Header(title = "Items from ${index + 1} to ${index + 10}") + } + result += ScrollableListItem.Item("Item ${index + 1}") + } + + return result +} + +private sealed class ScrollableListItem { + data class Header(val title: String) : ScrollableListItem() + data class Item(val title: String) : ScrollableListItem() +} From 4ee605a070d9dd8d915568bc6f3c9575435f8e63 Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 22:54:59 +0500 Subject: [PATCH 17/20] [klistnode] Removes detekt suppression --- .../io/github/kakaocup/compose/node/element/list/KListNode.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt index 694eac7c..3f132531 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt @@ -72,7 +72,6 @@ class KListNode( * @param itemIndexSemanticsPropertyKey Semantic property key for the list item index. * @param lengthSemanticsPropertyKey Semantic property key for the list length. */ - @Suppress("detekt.LongParameterList") constructor( semanticsProvider: SemanticsNodeInteractionsProvider, viewBuilderAction: ViewBuilder.() -> Unit, From 778b21db771b21c8449d0e9f946e0323af0ae4f4 Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Sun, 23 Jun 2024 23:08:15 +0500 Subject: [PATCH 18/20] [klistnode] Fixes error with scroll to index action --- .../io/github/kakaocup/compose/node/element/list/KListNode.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt index 3f132531..817c97ed 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt @@ -107,7 +107,7 @@ class KListNode( @OptIn(ExperimentalTestApi::class) override fun performScrollToIndex(index: Int) { check(isScrollable) - getItemAt(0) + getItemAt(index) } // endregion From c3a75210d5ec7c43713578c06786937d67815cb6 Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Mon, 24 Jun 2024 13:47:58 +0500 Subject: [PATCH 19/20] [klistnode] Fixes review comment -- replace 'requireSemanticsProvider' with pipeline of functions for more readability --- .../kakaocup/compose/node/core/BaseNode.kt | 12 ++--------- .../compose/node/element/ComposeScreen.kt | 4 +++- .../node/element/lazylist/KLazyListNode.kt | 14 +++++++++---- .../node/element/list/KListItemNode.kt | 4 +++- .../compose/node/element/list/KListNode.kt | 20 ++++++++++++++----- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index 8a4530b1..6afc1fc2 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -54,7 +54,7 @@ abstract class BaseNode> constructor( position = requireNodeMatcher().position, useUnmergedTree = requireNodeMatcher().useUnmergedTree ), - semanticsProvider = requireSemanticsProvider() + semanticsProvider = semanticsProvider.orGlobal().checkNotNull() ), parentDelegate = parentNode?.delegate ) @@ -66,7 +66,7 @@ abstract class BaseNode> constructor( NodeMatcher::class.java, BaseNode::class.java, ).newInstance( - requireSemanticsProvider(), + semanticsProvider.orGlobal().checkNotNull(), ViewBuilder().apply(function).build(), this, ) @@ -81,14 +81,6 @@ abstract class BaseNode> constructor( ?: throw KakaoComposeException("NodeMatcher is null: Provide via constructor or use `initSemantics` method`") } - /** - * Allowed getter for [requireSemanticsProvider]. - * Any [SemanticsNodeInteractionsProvider] must be initialized before use. - */ - fun requireSemanticsProvider(): SemanticsNodeInteractionsProvider { - return this.semanticsProvider.orGlobal().checkNotNull() - } - /** * Method for deferred initialization of [BaseNode] constructor parameters. * Simplifies the description of child nodes in list nodes. diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt index 8a3361d4..c9f7136f 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt @@ -6,6 +6,8 @@ import io.github.kakaocup.compose.node.core.ComposeMarker import io.github.kakaocup.compose.node.core.BaseNode import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder +import io.github.kakaocup.compose.utilities.checkNotNull +import io.github.kakaocup.compose.utilities.orGlobal @Suppress("UNCHECKED_CAST") @ComposeMarker @@ -33,7 +35,7 @@ open class ComposeScreen> : BaseNode { ) : super(semanticsProvider, nodeMatcher) fun onNode(viewBuilderAction: ViewBuilder.() -> Unit) = KNode( - requireSemanticsProvider(), + semanticsProvider.orGlobal().checkNotNull(), viewBuilderAction, ) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt index 2d7b25e5..abc50d2b 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt @@ -11,6 +11,8 @@ import io.github.kakaocup.compose.node.assertion.LazyListNodeAssertions import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder import io.github.kakaocup.compose.node.core.BaseNode +import io.github.kakaocup.compose.utilities.checkNotNull +import io.github.kakaocup.compose.utilities.orGlobal /** * Node class with special api to test Lazy List (LazyColumn or LazyRow) @@ -67,7 +69,9 @@ class KLazyListNode( }.provideItem performScrollToIndex(position) - val semanticsNode = requireSemanticsProvider() + val semanticsNode = semanticsProvider + .orGlobal() + .checkNotNull() .onNode(semanticsMatcher) .onChildren() .filterToOne(positionMatcher(position)) @@ -75,7 +79,7 @@ class KLazyListNode( function(provideItem( semanticsNode, - requireSemanticsProvider() + semanticsProvider.orGlobal().checkNotNull() ) as T) } @@ -96,7 +100,9 @@ class KLazyListNode( performScrollToNode(nodeMatcher.matcher) - val semanticsNode = requireSemanticsProvider() + val semanticsNode = semanticsProvider + .orGlobal() + .checkNotNull() .onNode(semanticsMatcher) .onChildren() .filter(nodeMatcher.matcher)[nodeMatcher.position] @@ -104,7 +110,7 @@ class KLazyListNode( return provideItem( semanticsNode, - requireSemanticsProvider() + semanticsProvider.orGlobal().checkNotNull() ) as T } diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt index 59eb766c..5d68902e 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt @@ -4,6 +4,8 @@ import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.test.SemanticsMatcher import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.core.BaseNode +import io.github.kakaocup.compose.utilities.checkNotNull +import io.github.kakaocup.compose.utilities.orGlobal /** * Base class for all child nodes within [KListNode]. @@ -33,7 +35,7 @@ open class KListItemNode> protected constructor() : Bas val instance = T::class.java.getDeclaredConstructor().newInstance() instance.initSemantics( - semanticsProvider = listNode.requireSemanticsProvider(), + semanticsProvider = listNode.semanticsProvider.orGlobal().checkNotNull(), nodeMatcher = NodeMatcher( matcher = SemanticsMatcher( description = "Semantics node id = ${semanticsNode.id}", diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt index 817c97ed..64dc6279 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt @@ -16,6 +16,8 @@ import io.github.kakaocup.compose.node.builder.NodeMatcher import io.github.kakaocup.compose.node.builder.ViewBuilder import io.github.kakaocup.compose.node.core.BaseNode import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.utilities.checkNotNull +import io.github.kakaocup.compose.utilities.orGlobal /** * A slightly modified copy of [io.github.kakaocup.compose.node.element.lazylist.KLazyListNode]. @@ -131,7 +133,9 @@ class KListNode( performScrollToNode(childMatcher.matcher) } } else { - requireSemanticsProvider() + semanticsProvider + .orGlobal() + .checkNotNull() .onNode(rootNodeMatcher, this.useUnmergedTree) .onChildren() .filter(childMatcher.matcher)[childMatcher.position] @@ -164,7 +168,9 @@ class KListNode( } } } else { - requireSemanticsProvider() + semanticsProvider + .orGlobal() + .checkNotNull() .onNode(rootNodeMatcher, this.useUnmergedTree) .onChildren() .filter(childMatcher.matcher)[childMatcher.position] @@ -384,7 +390,9 @@ class KListNode( performScrollToNode(childMatcher.matcher) } - val semanticsNode = requireSemanticsProvider() + val semanticsNode = semanticsProvider + .orGlobal() + .checkNotNull() .onNode(rootNodeMatcher, this.useUnmergedTree) .onChildren() .filter(childMatcher.matcher)[childMatcher.position] @@ -432,7 +440,9 @@ class KListNode( // Warning! // Within lazy collections, `filterToOne` cannot be used on child nodes. // In Compose version 1.5.5, the semantic tree of lazy collections MAY CONTAIN DUPLICATES. - val semanticsNode = requireSemanticsProvider() + val semanticsNode = semanticsProvider + .orGlobal() + .checkNotNull() .onNode(rootNodeMatcher, this.useUnmergedTree) .onChildren() .filter(childMatcher.matcher)[childMatcher.position] @@ -503,7 +513,7 @@ fun BaseNode<*>.KListNode( .build() return KListNode( - semanticsProvider = requireSemanticsProvider(), + semanticsProvider = this.semanticsProvider.orGlobal().checkNotNull(), nodeMatcher = nodeMatcher, parentNode = this, useUnmergedTree = useUnmergedTree, From 347212148e88df06a5e4498a0acfe7970b77e95e Mon Sep 17 00:00:00 2001 From: "p.strelchenko" Date: Mon, 24 Jun 2024 13:50:12 +0500 Subject: [PATCH 20/20] [klistnode] Fixes review comment -- replace 'requireNodeMatcher' with pipeline of functions for more readability --- .../kakaocup/compose/node/core/BaseNode.kt | 18 ++++-------------- .../kakaocup/compose/utilities/Extensions.kt | 6 +++++- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index 6afc1fc2..0aed2aae 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -3,7 +3,6 @@ package io.github.kakaocup.compose.node.core import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.hasAnyAncestor -import io.github.kakaocup.compose.exception.KakaoComposeException import io.github.kakaocup.compose.intercept.delegate.ComposeDelegate import io.github.kakaocup.compose.intercept.delegate.ComposeInterceptable import io.github.kakaocup.compose.node.action.NodeActions @@ -51,8 +50,8 @@ abstract class BaseNode> constructor( nodeProvider = NodeProvider( nodeMatcher = NodeMatcher( matcher = combineSemanticMatchers(), - position = requireNodeMatcher().position, - useUnmergedTree = requireNodeMatcher().useUnmergedTree + position = nodeMatcher.checkNotNull().position, + useUnmergedTree = nodeMatcher.checkNotNull().useUnmergedTree ), semanticsProvider = semanticsProvider.orGlobal().checkNotNull() ), @@ -72,15 +71,6 @@ abstract class BaseNode> constructor( ) } - /** - * Allowed getter for [nodeMatcher]. - * Any [NodeMatcher] must be initialized before use. - */ - fun requireNodeMatcher(): NodeMatcher { - return this.nodeMatcher - ?: throw KakaoComposeException("NodeMatcher is null: Provide via constructor or use `initSemantics` method`") - } - /** * Method for deferred initialization of [BaseNode] constructor parameters. * Simplifies the description of child nodes in list nodes. @@ -103,10 +93,10 @@ abstract class BaseNode> constructor( var parent = this.parentNode while (parent != null) { - semanticsMatcherList.add(hasAnyAncestor(parent.requireNodeMatcher().matcher)) + semanticsMatcherList.add(hasAnyAncestor(parent.nodeMatcher.checkNotNull().matcher)) parent = parent.parentNode } - semanticsMatcherList.add(this.requireNodeMatcher().matcher) + semanticsMatcherList.add(this.nodeMatcher.checkNotNull().matcher) return semanticsMatcherList.reduce { finalMatcher, matcher -> finalMatcher and matcher } } diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt index 614ca258..938c947b 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt +++ b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt @@ -3,9 +3,13 @@ package io.github.kakaocup.compose.utilities import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import io.github.kakaocup.compose.KakaoCompose import io.github.kakaocup.compose.exception.KakaoComposeException +import io.github.kakaocup.compose.node.builder.NodeMatcher fun SemanticsNodeInteractionsProvider?.orGlobal(): SemanticsNodeInteractionsProvider? = this ?: KakaoCompose.Global.semanticsProvider fun SemanticsNodeInteractionsProvider?.checkNotNull(): SemanticsNodeInteractionsProvider = - this ?: throw KakaoComposeException("SemanticsProvider is null: Provide via constructor or use KakaoComposeTestRule") \ No newline at end of file + this ?: throw KakaoComposeException("SemanticsProvider is null: Provide via constructor or use KakaoComposeTestRule") + +fun NodeMatcher?.checkNotNull(): NodeMatcher = + this ?: throw KakaoComposeException("NodeMatcher is null: Provide via constructor or use 'initSemantics' method") \ No newline at end of file