Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KListNode implementation #85

Merged
merged 22 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fa920e6
feat(core): override global useUnmergedTree
Vacxe Jun 4, 2024
70d2ebe
Merge pull request #83 from KakaoCup/use-unmerged-tree-global
Vacxe Jun 4, 2024
2a5468a
docs(readme): override global useUnmergedTree
Vacxe Jun 4, 2024
bff2ea9
feat(core): add global semantics provider
Vacxe Jun 11, 2024
7d0eddb
feat(core): add extension for global sematic provider usage
Vacxe Jun 11, 2024
e2b5271
Merge pull request #84 from KakaoCup/add-global-semantics-provider
Vacxe Jun 18, 2024
d5f85c9
[klistnode] Adds allowed getter for SemanticsNodeInteractionsProvider
PStrelchenko Jun 23, 2024
e15dcd5
[klistnode] Adds allowed getter for NodeMatcher in BaseNode
PStrelchenko Jun 23, 2024
4c1175b
[klistnode] Adds method for deffered initialization of BaseNode const…
PStrelchenko Jun 23, 2024
39117d6
[klistnode] Adds KNode for list items
PStrelchenko Jun 23, 2024
8160125
[klistnode] Adds KNode for lists
PStrelchenko Jun 23, 2024
517a78c
[klistnode] Adds semantics keys for list item index and list length f…
PStrelchenko Jun 23, 2024
ae54a95
[klistnode] Fixes function names collision in BaseNode
PStrelchenko Jun 23, 2024
00911d2
[klistnode] Changes packaging options for sample app to enable Layout…
PStrelchenko Jun 23, 2024
b5e8333
[klistnode] Takes into account the value of a global variable Kakao.O…
PStrelchenko Jun 23, 2024
b60f80e
[klistnode] Overrides performScrollToIndex action to a more reliable …
PStrelchenko Jun 23, 2024
cef68cb
[klistnode] Adds addtional factory methods for KListNode
PStrelchenko Jun 23, 2024
c19cfb7
[klistnode] Adds tests for KListNode
PStrelchenko Jun 23, 2024
4ee605a
[klistnode] Removes detekt suppression
PStrelchenko Jun 23, 2024
778b21d
[klistnode] Fixes error with scroll to index action
PStrelchenko Jun 23, 2024
c3a7521
[klistnode] Fixes review comment -- replace 'requireSemanticsProvider…
PStrelchenko Jun 24, 2024
3472121
[klistnode] Fixes review comment -- replace 'requireNodeMatcher' with…
PStrelchenko Jun 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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

object KakaoCompose {
internal var composeInterceptor: Interceptor<ComposeInteraction, ComposeAssertion, ComposeAction>? = null
internal var composeInterceptor: Interceptor<ComposeInteraction, ComposeAssertion, ComposeAction>? =
null

/**
* Operator that allows usage of DSL style
Expand Down Expand Up @@ -37,6 +40,27 @@ object KakaoCompose {
* @see intercept
* @see Interceptor
*/

/**
* Global overrides for default Espresso behaviour
*/
object Override {
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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.kakaocup.compose.exception

class KakaoComposeException(message: String) : Exception(message)
Original file line number Diff line number Diff line change
@@ -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<Int>

/**
* 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
}
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ 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<out T : BaseNode<T>> constructor(
@PublishedApi internal val semanticsProvider: SemanticsNodeInteractionsProvider,
private val nodeMatcher: NodeMatcher,
private val parentNode: BaseNode<*>? = null,
@PublishedApi internal var semanticsProvider: SemanticsNodeInteractionsProvider? = null,
private var nodeMatcher: NodeMatcher? = null,
private var parentNode: BaseNode<*>? = null,
) : KDSL<T>,
NodeAssertions,
TextResourcesNodeAssertions,
Expand All @@ -26,7 +28,7 @@ abstract class BaseNode<out T : BaseNode<T>> constructor(
ComposeInterceptable {

constructor(
semanticsProvider: SemanticsNodeInteractionsProvider,
semanticsProvider: SemanticsNodeInteractionsProvider? = null,
viewBuilderAction: ViewBuilder.() -> Unit,
) : this(
semanticsProvider = semanticsProvider,
Expand All @@ -35,7 +37,7 @@ abstract class BaseNode<out T : BaseNode<T>> constructor(
)

constructor(
semanticsProvider: SemanticsNodeInteractionsProvider,
semanticsProvider: SemanticsNodeInteractionsProvider? = null,
nodeMatcher: NodeMatcher,
) : this(
semanticsProvider = semanticsProvider,
Expand All @@ -48,10 +50,10 @@ abstract class BaseNode<out T : BaseNode<T>> constructor(
nodeProvider = NodeProvider(
nodeMatcher = NodeMatcher(
matcher = combineSemanticMatchers(),
position = nodeMatcher.position,
useUnmergedTree = nodeMatcher.useUnmergedTree
position = nodeMatcher.checkNotNull().position,
useUnmergedTree = nodeMatcher.checkNotNull().useUnmergedTree
),
semanticsProvider = semanticsProvider
semanticsProvider = semanticsProvider.orGlobal().checkNotNull()
),
parentDelegate = parentNode?.delegate
)
Expand All @@ -63,12 +65,26 @@ abstract class BaseNode<out T : BaseNode<T>> constructor(
NodeMatcher::class.java,
BaseNode::class.java,
).newInstance(
semanticsProvider,
semanticsProvider.orGlobal().checkNotNull(),
ViewBuilder().apply(function).build(),
this,
)
}

/**
* 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
*/
Expand All @@ -77,10 +93,10 @@ abstract class BaseNode<out T : BaseNode<T>> constructor(
var parent = this.parentNode

while (parent != null) {
semanticsMatcherList.add(hasAnyAncestor(parent.nodeMatcher.matcher))
semanticsMatcherList.add(hasAnyAncestor(parent.nodeMatcher.checkNotNull().matcher))
parent = parent.parentNode
}
semanticsMatcherList.add(this.nodeMatcher.matcher)
semanticsMatcherList.add(this.nodeMatcher.checkNotNull().matcher)

return semanticsMatcherList.reduce { finalMatcher, matcher -> finalMatcher and matcher }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,56 @@ 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
open class ComposeScreen<out T : ComposeScreen<T>> : BaseNode<T> {

internal constructor(
semanticsProvider: SemanticsNodeInteractionsProvider,
semanticsProvider: SemanticsNodeInteractionsProvider? = null,
nodeMatcher: NodeMatcher,
parentNode: ComposeScreen<T>? = 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(
semanticsProvider.orGlobal().checkNotNull(),
viewBuilderAction,
)

companion object {
inline fun <reified T : ComposeScreen<T>> onComposeScreen(
semanticsProvider: SemanticsNodeInteractionsProvider,
noinline function: T.() -> Unit
): T {
return T::class.java
.getDeclaredConstructor(
SemanticsNodeInteractionsProvider::class.java
)
.newInstance(semanticsProvider)
noinline function: T.() -> Unit,
): T = T::class.java
.getDeclaredConstructor(
SemanticsNodeInteractionsProvider::class.java
)
.newInstance(semanticsProvider)
.apply { this(function) }

inline fun <reified T : ComposeScreen<T>> onComposeScreen(
noinline function: T.() -> Unit,
): T =
T::class.java.getDeclaredConstructor()
.newInstance()
.apply { this(function) }
}

}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
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
import io.github.kakaocup.compose.node.core.BaseNode

abstract class KIconNode(
semanticsProvider: SemanticsNodeInteractionsProvider,
semanticsProvider: SemanticsNodeInteractionsProvider? = null,
nodeMatcher: NodeMatcher,
parentNode: BaseNode<*>?,
useUnmergedTree: Boolean = false
useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false
) : BaseNode<KIconNode>(
semanticsProvider = semanticsProvider,
nodeMatcher = nodeMatcher.copy(useUnmergedTree = useUnmergedTree),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
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

open class KNode : BaseNode<KNode> {

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)
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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

abstract class KTextNode(
semanticsProvider: SemanticsNodeInteractionsProvider,
semanticsProvider: SemanticsNodeInteractionsProvider? = null,
nodeMatcher: NodeMatcher,
parentNode: BaseNode<*>?,
useUnmergedTree: Boolean = false
useUnmergedTree: Boolean = KakaoCompose.Override.useUnmergedTree ?: false
) : BaseNode<KTextNode>(
semanticsProvider = semanticsProvider,
nodeMatcher = nodeMatcher.copy(useUnmergedTree = useUnmergedTree),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import io.github.kakaocup.compose.node.core.BaseNode
*/
abstract class KLazyListItemNode<out T : KLazyListItemNode<T>>(
semanticNode: SemanticsNode,
semanticsProvider: SemanticsNodeInteractionsProvider,
semanticsProvider: SemanticsNodeInteractionsProvider? = null,
) : BaseNode<T>(
semanticsProvider,
NodeMatcher(
Expand Down
Loading
Loading