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

Combine semantic matchers from all ancestor nodes #28

Merged
merged 2 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package io.github.kakaocup.compose.node.core

import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasAnyAncestor
import io.github.kakaocup.compose.intercept.delegate.ComposeDelegate
import io.github.kakaocup.compose.intercept.delegate.ComposeInterceptable
import io.github.kakaocup.compose.node.action.NodeActions
Expand All @@ -15,7 +16,7 @@ import io.github.kakaocup.compose.node.builder.ViewBuilder
abstract class BaseNode<out T : BaseNode<T>> constructor(
@PublishedApi internal val semanticsProvider: SemanticsNodeInteractionsProvider,
private val nodeMatcher: NodeMatcher,
parentNode: BaseNode<*>? = null,
private val parentNode: BaseNode<*>? = null,
) : KDSL<T>, NodeAssertions, NodeActions, TextActions, ComposeInterceptable {

constructor(
Expand All @@ -40,7 +41,7 @@ abstract class BaseNode<out T : BaseNode<T>> constructor(
ComposeDelegate(
nodeProvider = NodeProvider(
nodeMatcher = NodeMatcher(
matcher = if (parentNode == null) nodeMatcher.matcher else hasParent(parentNode.nodeMatcher.matcher) and nodeMatcher.matcher,
matcher = combineSemanticMatchers(),
position = nodeMatcher.position,
useUnmergedTree = nodeMatcher.useUnmergedTree
),
Expand All @@ -61,4 +62,20 @@ abstract class BaseNode<out T : BaseNode<T>> constructor(
this,
)
}
}

/***
* Combines semantic matchers from all ancestor nodes
*/
private fun combineSemanticMatchers(): SemanticsMatcher {
matzuk marked this conversation as resolved.
Show resolved Hide resolved
val semanticsMatcherList = mutableListOf<SemanticsMatcher>()
var parent = this.parentNode

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

return semanticsMatcherList.reduce { finalMatcher, matcher -> finalMatcher and matcher }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.kakaocup.compose.screen.navigation

import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode

class FirstNavigationScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen<FirstNavigationScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag("FirstNavigationScreen") }
) {
val sharedWidget: SharedWidgetNode = child {
hasTestTag("SharedWidget")
}

val navigationButton: KNode = child {
hasTestTag("NavigateSecondScreenButton")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.kakaocup.compose.screen.navigation

import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import io.github.kakaocup.compose.node.element.ComposeScreen

class SecondNavigationScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen<SecondNavigationScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag("SecondNavigationScreen") }
) {
val sharedWidget: SharedWidgetNode = child {
hasTestTag("SharedWidget")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.github.kakaocup.compose.screen.navigation

import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import io.github.kakaocup.compose.node.builder.NodeMatcher
import io.github.kakaocup.compose.node.core.BaseNode
import io.github.kakaocup.compose.node.element.KNode

class SharedWidgetNode(
semanticsProvider: SemanticsNodeInteractionsProvider,
nodeMatcher: NodeMatcher,
parentNode: BaseNode<*>? = null
) : BaseNode<SharedWidgetNode>(semanticsProvider, nodeMatcher, parentNode) {
val title: KNode = child {
hasTestTag("SharedWidgetTitle")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.github.kakaocup.compose.test

import androidx.compose.material.MaterialTheme
import androidx.compose.ui.test.junit4.createComposeRule
import io.github.kakaocup.compose.SimpleNavigationScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.screen.navigation.FirstNavigationScreen
import io.github.kakaocup.compose.screen.navigation.SecondNavigationScreen
import org.junit.Rule
import org.junit.Test

class SimpleNavigationTest {
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun simpleNavigationTest() {
composeTestRule.setContent {
MaterialTheme {
SimpleNavigationScreen()
}
}

onComposeScreen<FirstNavigationScreen>(composeTestRule) {
sharedWidget.title {
assertIsDisplayed()
assertTextEquals("First screen")
}
navigationButton.performClick()
}

onComposeScreen<SecondNavigationScreen>(composeTestRule) {
sharedWidget.title {
assertIsDisplayed()
assertTextEquals("Second screen")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.github.kakaocup.compose

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.Card
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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

@Composable
fun SimpleNavigationScreen() {
var currentScreen by rememberSaveable { mutableStateOf(NavigationScreen.First) }

when (currentScreen) {
NavigationScreen.First -> FirstNavigationScreen(
onNavigateSecondScreen = {
currentScreen = NavigationScreen.Second
}
)
NavigationScreen.Second -> SecondNavigationScreen()
}
}

@Composable
private fun FirstNavigationScreen(onNavigateSecondScreen: () -> Unit) {
Scaffold(Modifier.testTag("FirstNavigationScreen")) {
Card(Modifier.padding(64.dp)) {
Box(
Modifier
.fillMaxSize()
.padding(32.dp)
) {
SharedWidget(
title = "First screen",
modifier = Modifier.align(Alignment.TopCenter)
)
Button(
onClick = onNavigateSecondScreen,
modifier = Modifier
.align(Alignment.BottomCenter)
.testTag("NavigateSecondScreenButton")
) {
Text("Navigate second screen")
}
}
}
}
}

@Composable
private fun SecondNavigationScreen() {
Scaffold(Modifier.testTag("SecondNavigationScreen")) {
Card(Modifier.padding(64.dp)) {
Box(
Modifier
.fillMaxSize()
.padding(32.dp)
) {
SharedWidget(
title = "Second screen",
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}
}

@Composable
private fun SharedWidget(title: String, modifier: Modifier = Modifier) {
Box(
modifier
.clip(RoundedCornerShape(16.dp))
.background(Color.Green)
.testTag("SharedWidget")
) {
Text(
title,
Modifier
.padding(16.dp)
.testTag("SharedWidgetTitle")
)
}
}

private enum class NavigationScreen {
First,
Second
}