From d3cc9cd842720955c2fb052cffa60f9668105f39 Mon Sep 17 00:00:00 2001 From: Aleksei Tiurin Date: Thu, 7 Mar 2024 02:14:33 +0200 Subject: [PATCH] Compose List Item Child (#60) * Compose List Item Child * Simplify list child declaration lazy { getChild() } -> child {} --- .../sampleapp/pages/ComposeListPage.kt | 7 +- .../sampleapp/pages/FriendsListPage.kt | 2 +- .../tests/compose/ComposeListTest.kt | 57 ++++++++++----- .../ComposeListWithPositionTestTagTest.kt | 17 +++-- .../atiurin/sampleapp/compose/ContacsList.kt | 71 ++++++++++--------- .../core/compose/ComposeRuleContainer.kt | 4 +- .../core/compose/list/UltronComposeList.kt | 6 +- .../compose/list/UltronComposeListItem.kt | 5 ++ .../UltronComposeSemanticsNodeInteraction.kt | 11 +++ .../compose/operation/ComposeOperationType.kt | 3 +- .../ultron/extensions/SemanticsNodeExt.kt | 23 ++++++ .../extensions/SemanticsNodeInteractionExt.kt | 30 +++++++- .../ultron/extensions/SemanticsSelectorExt.kt | 19 +++++ .../recyclerview/UltronRecyclerViewItem.kt | 4 ++ 14 files changed, 191 insertions(+), 68 deletions(-) create mode 100644 ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsNodeExt.kt create mode 100644 ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsSelectorExt.kt diff --git a/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeListPage.kt b/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeListPage.kt index 59fba3a1..b37ffeb3 100644 --- a/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeListPage.kt +++ b/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/ComposeListPage.kt @@ -32,7 +32,10 @@ object ComposeListPage : Page() { fun getContactItemByTestTag(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasTestTag(getContactItemTestTagById(contact))) fun getContactItemByName(contact: Contact): ComposeFriendListItem = lazyList.getItem(hasAnyDescendant(hasText(contact.name) and hasTestTag(contactNameTestTag))) class ComposeFriendListItem : UltronComposeListItem() { - val name by lazy { getChild(hasTestTag(contactNameTestTag)) } + val name by child { hasTestTag(contactNameTestTag) } val status by lazy { getChild(hasTestTag(contactStatusTestTag)) } + val notExisted by child { hasTestTag("NotExistedChild") } } -} \ No newline at end of file +} + + diff --git a/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/FriendsListPage.kt b/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/FriendsListPage.kt index eff3d9da..c93e0749 100644 --- a/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/FriendsListPage.kt +++ b/sample-app/src/androidTest/java/com/atiurin/sampleapp/pages/FriendsListPage.kt @@ -30,7 +30,7 @@ object FriendsListPage : Page() { } class FriendRecyclerItem : UltronRecyclerViewItem() { - val name by lazy { getChild(withId(R.id.tv_name)) } + val name by child { withId(R.id.tv_name) } val status by lazy { getChild(withId(R.id.tv_status)) } val avatar by lazy { getChild(withId(R.id.avatar)) } } diff --git a/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListTest.kt b/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListTest.kt index 8dbfdbce..8e103dda 100644 --- a/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListTest.kt +++ b/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListTest.kt @@ -2,12 +2,18 @@ package com.atiurin.sampleapp.tests.compose import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.test.* +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.performScrollToIndex import com.atiurin.sampleapp.activity.ComposeListActivity -import com.atiurin.sampleapp.compose.* +import com.atiurin.sampleapp.compose.contactStatusTestTag +import com.atiurin.sampleapp.compose.contactsListContentDesc +import com.atiurin.sampleapp.compose.contactsListHeaderTag +import com.atiurin.sampleapp.compose.contactsListTestTag +import com.atiurin.sampleapp.compose.getContactItemTestTagById import com.atiurin.sampleapp.data.repositories.CONTACTS import com.atiurin.sampleapp.data.repositories.ContactRepositoty import com.atiurin.sampleapp.framework.utils.AssertUtils @@ -18,14 +24,15 @@ import com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption import com.atiurin.ultron.core.common.options.TextContainsOption import com.atiurin.ultron.core.compose.createUltronComposeRule import com.atiurin.ultron.core.compose.list.composeList +import com.atiurin.ultron.extensions.assertIsDisplayed import com.atiurin.ultron.extensions.assertTextEquals import com.atiurin.ultron.extensions.click -import com.atiurin.ultron.extensions.assertIsDisplayed +import com.atiurin.ultron.extensions.findNodeInTree import org.junit.Assert import org.junit.Rule import org.junit.Test -class ComposeListTest: BaseTest() { +class ComposeListTest : BaseTest() { @get:Rule val composeRule = createUltronComposeRule() @@ -38,22 +45,25 @@ class ComposeListTest: BaseTest() { fun item_existItem() { val index = 20 val contact = CONTACTS[index] - listWithMergedTree.item(hasText(contact.name)) + listWithMergedTree.item(hasAnyDescendant(hasText(contact.name))) .assertIsDisplayed() - .assertTextContains(contact.name) + .assertMatches(hasAnyDescendant(hasText(contact.name))) } @Test fun item_notExistItem() { - AssertUtils.assertException { listWithMergedTree.item(hasText("123gakshgdasl kgas")).assertIsDisplayed() } + AssertUtils.assertException { + listWithMergedTree.item(hasText("123gakshgdasl kgas")).assertIsDisplayed() + } } @Test fun visibleItem_indexInScope() { val index = 2 val contact = CONTACTS[index] - listWithMergedTree.visibleItem(index) - .assertTextContains(contact.name) + listWithMergedTree.visibleItem(index).printToLog("ULTRON") + .assertMatches(hasAnyDescendant(hasText(contact.name))) + } @Test @@ -68,8 +78,8 @@ class ComposeListTest: BaseTest() { val contact = CONTACTS[0] listWithMergedTree.firstVisibleItem() .assertIsDisplayed() - .assertTextContains(contact.name) - .assertTextContains(contact.status) + .assertMatches(hasAnyDescendant(hasText(contact.name))) + .assertMatches(hasAnyDescendant(hasText(contact.status))) } @Test @@ -117,7 +127,7 @@ class ComposeListTest: BaseTest() { } hasText(contact.name).assertIsDisplayed() Assert.assertTrue(children.size > 10) - val child = children.find { child -> child.config[SemanticsProperties.Text].any { it.text == contact.name } } + val child = children.findNodeInTree(hasText(contact.name)) Assert.assertNotNull(child) } @@ -256,18 +266,18 @@ class ComposeListTest: BaseTest() { } @Test - fun assertVisibleItemsCount_properCountProvided(){ + fun assertVisibleItemsCount_properCountProvided() { val count = listWithMergedTree.getVisibleItemsCount() listWithMergedTree.assertVisibleItemsCount(count) } @Test - fun assertVisibleItemsCount_invalidCountProvided(){ + fun assertVisibleItemsCount_invalidCountProvided() { AssertUtils.assertException { listWithMergedTree.withTimeout(1000).assertVisibleItemsCount(100) } } @Test - fun itemByPosition_propertyConfiguredTest(){ + fun itemByPosition_propertyConfiguredTest() { val index = 20 val contact = CONTACTS[index] val item = listPage.lazyList.item(20).assertIsDisplayed() @@ -275,7 +285,7 @@ class ComposeListTest: BaseTest() { } @Test - fun getItemByPosition_propertyConfiguredTest(){ + fun getItemByPosition_propertyConfiguredTest() { val index = 20 val contact = CONTACTS[index] listPage.getItemByPosition(index).apply { @@ -286,18 +296,27 @@ class ComposeListTest: BaseTest() { } @Test - fun assertItemDoesNotExistWithSearch_NotExistedItem(){ + fun assertItemDoesNotExistWithSearch_NotExistedItem() { listWithMergedTree.assertItemDoesNotExist(hasText("NOT EXISTED TeXT")) } @Test - fun assertItemDoesNotExistWithSearch_ExistedItem(){ + fun assertItemDoesNotExistWithSearch_ExistedItem() { val contact = ContactRepositoty.getLast() AssertUtils.assertException { listWithMergedTree.withTimeout(2000).assertItemDoesNotExist(hasText(contact.name)) } } + @Test + fun getItem_NotExistedItemChild() { + val index = 20 + val contact = CONTACTS[index] + listPage.getContactItemByName(contact).apply { + AssertUtils.assertException { notExisted.withTimeout(1000).assertIsDisplayed() } + } + } + private fun setEmptyListContent() { composeRule.setContent { LazyColumn( diff --git a/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListWithPositionTestTagTest.kt b/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListWithPositionTestTagTest.kt index ea0d23b3..88ca6004 100644 --- a/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListWithPositionTestTagTest.kt +++ b/sample-app/src/androidTest/java/com/atiurin/sampleapp/tests/compose/ComposeListWithPositionTestTagTest.kt @@ -1,7 +1,9 @@ package com.atiurin.sampleapp.tests.compose +import androidx.compose.ui.test.hasAnyDescendant import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText import com.atiurin.sampleapp.activity.ComposeListWithPositionTestTagActivity import com.atiurin.sampleapp.compose.ListItemPositionPropertyKey import com.atiurin.sampleapp.compose.contactsListContentDesc @@ -26,7 +28,7 @@ class ComposeListWithPositionTestTagTest { val contact = CONTACTS[index] list.item(hasTestTag(getContactItemTestTagByPosition(index))) .assertIsDisplayed() - .assertTextContains(contact.name) + .assertMatches(hasAnyDescendant(hasText(contact.name))) } @Test @@ -35,30 +37,31 @@ class ComposeListWithPositionTestTagTest { val contact = CONTACTS[count] list.lastVisibleItem() .assertIsDisplayed() - .assertTextContains(contact.name) - .assertTextContains(contact.status) + .assertMatches(hasAnyDescendant(hasText(contact.name))) + .assertMatches(hasAnyDescendant(hasText(contact.status))) } + @Test - fun itemByPosition_propertyNOTConfiguredInTest(){ + fun itemByPosition_propertyNOTConfiguredInTest() { AssertUtils.assertException { list.item(20).assertIsDisplayed() } } @Test - fun itemByPosition_propertyNOTConfiguredInApplication(){ + fun itemByPosition_propertyNOTConfiguredInApplication() { AssertUtils.assertException { composeListWithProperty.withTimeout(1000).item(20).assertIsDisplayed() } } @Test - fun getItemByPosition_propertyNOTConfiguredInTest(){ + fun getItemByPosition_propertyNOTConfiguredInTest() { AssertUtils.assertException { list.getItem(20).assertIsDisplayed() } } @Test - fun getItemByPosition_propertyNOTConfiguredInApplication(){ + fun getItemByPosition_propertyNOTConfiguredInApplication() { AssertUtils.assertException { composeListWithProperty.withTimeout(1000).getItem(20).assertIsDisplayed() } diff --git a/sample-app/src/main/java/com/atiurin/sampleapp/compose/ContacsList.kt b/sample-app/src/main/java/com/atiurin/sampleapp/compose/ContacsList.kt index d2ab19e6..e6a8e23e 100644 --- a/sample-app/src/main/java/com/atiurin/sampleapp/compose/ContacsList.kt +++ b/sample-app/src/main/java/com/atiurin/sampleapp/compose/ContacsList.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -58,9 +59,7 @@ fun ContactsList( testTagProvider: (Contact, Int) -> String, modifierProvider: (Int) -> Modifier, ) { - var selectedItem = remember { - mutableStateOf("") - } + val selectedItem = remember { mutableStateOf("") } Text(text = "Selected item = ${selectedItem.value}") LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), @@ -76,39 +75,43 @@ fun ContactsList( } } itemsIndexed(contacts, key = { _, c -> c.name }) { index, contact -> - Column( - modifier = modifierProvider.invoke(index) - .then(Modifier.clickable { - selectedItem.value = contact.name - val intent = Intent(context, ComposeSecondActivity::class.java) - intent.putExtra(ComposeSecondActivity.INTENT_CONTACT_ID, contact.id) - ContextCompat.startActivity(context, intent, null) - }) - .then(Modifier.semantics { - testTag = testTagProvider.invoke(contact, index) - }) + Box(modifier = modifierProvider + .invoke(index) + .semantics { + testTag = testTagProvider.invoke(contact, index) + } ) { - Row { - Image( - painter = painterResource(contact.avatar), - contentDescription = "avatar", - contentScale = ContentScale.Crop, // crop the image if it's not a square - modifier = Modifier - .size(80.dp) - .clip(CircleShape) // clip to the circle shape - .border(2.dp, Color.Transparent, CircleShape) // add a border (optional) - ) - Spacer(modifier = Modifier.width(16.dp)) - Column { - Text(contact.name, Modifier.semantics { testTag = contactNameTestTag }, fontSize = TextUnit(20f, TextUnitType.Sp)) - Spacer(modifier = Modifier.height(8.dp)) - Text(text = contact.status, Modifier.semantics { testTag = contactStatusTestTag }, fontSize = TextUnit(16f, TextUnitType.Sp)) - Spacer(modifier = Modifier.height(8.dp)) - } + Column( + modifier = Modifier + .then(Modifier.clickable { + selectedItem.value = contact.name + val intent = Intent(context, ComposeSecondActivity::class.java) + intent.putExtra(ComposeSecondActivity.INTENT_CONTACT_ID, contact.id) + ContextCompat.startActivity(context, intent, null) + }) + ) { + Row { + Image( + painter = painterResource(contact.avatar), + contentDescription = "avatar", + contentScale = ContentScale.Crop, // crop the image if it's not a square + modifier = Modifier + .size(80.dp) + .clip(CircleShape) // clip to the circle shape + .border(2.dp, Color.Transparent, CircleShape) // add a border (optional) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text(contact.name, Modifier.semantics { testTag = contactNameTestTag }, fontSize = TextUnit(20f, TextUnitType.Sp)) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = contact.status, Modifier.semantics { testTag = contactStatusTestTag }, fontSize = TextUnit(16f, TextUnitType.Sp)) + Spacer(modifier = Modifier.height(8.dp)) + } + } + Spacer(modifier = Modifier.height(8.dp)) + Divider(color = Color.Black) } - Spacer(modifier = Modifier.height(8.dp)) - Divider(color = Color.Black) } } } @@ -122,4 +125,4 @@ val ListItemPositionPropertyKey = SemanticsPropertyKey("ListItemPosition") var SemanticsPropertyReceiver.listItemPosition by ListItemPositionPropertyKey fun Modifier.listItemPosition(position: Int): Modifier { return semantics { listItemPosition = position } -} +} \ No newline at end of file diff --git a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/ComposeRuleContainer.kt b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/ComposeRuleContainer.kt index d89667c6..440410aa 100644 --- a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/ComposeRuleContainer.kt +++ b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/ComposeRuleContainer.kt @@ -1,6 +1,9 @@ package com.atiurin.ultron.core.compose import androidx.activity.ComponentActivity +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionCollection +import androidx.compose.ui.test.SemanticsSelector import androidx.compose.ui.test.TestContext import androidx.compose.ui.test.junit4.* import androidx.test.ext.junit.rules.ActivityScenarioRule @@ -80,4 +83,3 @@ fun createEmptyUltronComposeRule(): ComposeTestRule { fun ComposeTestRule.getTestContext() = this.getProperty("testContext") - diff --git a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/list/UltronComposeList.kt b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/list/UltronComposeList.kt index ac1c8c6d..6976f14e 100644 --- a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/list/UltronComposeList.kt +++ b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/list/UltronComposeList.kt @@ -15,6 +15,7 @@ import com.atiurin.ultron.core.compose.operation.ComposeOperationType import com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams import com.atiurin.ultron.exceptions.UltronAssertionException import com.atiurin.ultron.exceptions.UltronException +import com.atiurin.ultron.extensions.findNodeInTree import com.atiurin.ultron.utils.AssertUtils import org.junit.Assert @@ -51,6 +52,7 @@ class UltronComposeList( } return UltronComposeListItem(this, position, true) } + fun firstItem(): UltronComposeListItem = item(0) /** @@ -147,7 +149,7 @@ class UltronComposeList( ) { listInteraction -> listInteraction.performScrollToNode(itemMatcher) .onChildren().filterToOne(itemMatcher) - .onChildren().filterToOne(childMatcher) + .findNodeInTree(childMatcher, useUnmergedTree) } ) @@ -171,7 +173,7 @@ class UltronComposeList( operationType = ComposeOperationType.GET_LIST_ITEM_CHILD ) ) { listInteraction -> - listInteraction.onChildAt(index).onChildren().filterToOne(childMatcher) + listInteraction.onChildAt(index).findNodeInTree(childMatcher, useUnmergedTree) } ) diff --git a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt index 94be42d3..6e9f6f4c 100644 --- a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt +++ b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt @@ -68,6 +68,10 @@ open class UltronComposeListItem { */ protected constructor() + fun child(block: () -> SemanticsMatcher): Lazy = lazy { + getChild(block()) + } + fun setExecutor(ultronComposeList: UltronComposeList, itemMatcher: SemanticsMatcher) { this.executor = MatcherComposeItemExecutor(ultronComposeList, itemMatcher) } @@ -129,6 +133,7 @@ open class UltronComposeListItem { fun inputTextSelection(selection: TextRange) = apply { getItemUltronComposeInteraction().inputTextSelection(selection) } fun clearText() = apply { getItemUltronComposeInteraction().clearText() } fun replaceText(text: String) = apply { getItemUltronComposeInteraction().replaceText(text) } + fun printToLog(tag: String, maxDepth: Int = Int.MAX_VALUE) = apply { getItemUltronComposeInteraction().printToLog(tag, maxDepth) } @OptIn(ExperimentalTestApi::class) fun performMouseInput(block: MouseInjectionScope.() -> Unit) = apply { getItemUltronComposeInteraction().performMouseInput(block) } diff --git a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeSemanticsNodeInteraction.kt b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeSemanticsNodeInteraction.kt index 58e351d3..54c3ed1d 100644 --- a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeSemanticsNodeInteraction.kt +++ b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/nodeinteraction/UltronComposeSemanticsNodeInteraction.kt @@ -531,6 +531,16 @@ open class UltronComposeSemanticsNodeInteraction constructor( it.fetchSemanticsNode().config[key] } + + fun printToLog(tag: String, maxDepth: Int = Int.MAX_VALUE) = apply { + executeOperation( + operationBlock = { semanticsNodeInteraction.printToLog(tag, maxDepth) }, + name = "Print Semantics info to log for '${elementInfo.name}'", + type = PRINT_TO_LOG, + description = "Compose printToLog for '${elementInfo.name}' during $timeoutMs ms", + ) + } + //assertions fun assertIsDisplayed() = apply { executeOperation( @@ -797,6 +807,7 @@ open class UltronComposeSemanticsNodeInteraction constructor( operationDescription = "Anonymous Compose operation on '${elementInfo.name}' during $timeoutMs ms", ) + companion object { /** * Executes any compose action inside Ultron lifecycle diff --git a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/operation/ComposeOperationType.kt b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/operation/ComposeOperationType.kt index 69019173..bb8377f1 100644 --- a/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/operation/ComposeOperationType.kt +++ b/ultron-compose/src/main/java/com/atiurin/ultron/core/compose/operation/ComposeOperationType.kt @@ -25,5 +25,6 @@ enum class ComposeOperationType : UltronOperationType { PROGRESS_BAR_RANGE_EQUALS, ASSERT_MATCHES, CAPTURE_IMAGE, - GET_LIST_ITEM, GET_LIST_ITEM_CHILD, ASSERT_LIST_ITEM_DOES_NOT_EXIST + GET_LIST_ITEM, GET_LIST_ITEM_CHILD, ASSERT_LIST_ITEM_DOES_NOT_EXIST, + PRINT_TO_LOG } \ No newline at end of file diff --git a/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsNodeExt.kt b/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsNodeExt.kt new file mode 100644 index 00000000..56c6e142 --- /dev/null +++ b/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsNodeExt.kt @@ -0,0 +1,23 @@ +package com.atiurin.ultron.extensions + +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.test.SemanticsMatcher + +fun Iterable.findNodeInTree(matcher: SemanticsMatcher): List { + val targetNodes = mutableListOf() + this.forEach { node -> + targetNodes.addAll(node.findNodeInTree(matcher)) + } + return targetNodes +} + +fun SemanticsNode.findNodeInTree(matcher: SemanticsMatcher): List { + val targetNodes = mutableListOf() + if (matcher.matches(this)) { + targetNodes.add(this) + return targetNodes + } else { + targetNodes.addAll(this.children.findNodeInTree(matcher)) + } + return targetNodes +} \ No newline at end of file diff --git a/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsNodeInteractionExt.kt b/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsNodeInteractionExt.kt index 84cec08b..a48f4d2b 100644 --- a/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsNodeInteractionExt.kt +++ b/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsNodeInteractionExt.kt @@ -2,11 +2,27 @@ package com.atiurin.ultron.extensions import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionCollection import androidx.compose.ui.test.SemanticsSelector +import androidx.compose.ui.test.TestContext +import com.atiurin.ultron.exceptions.UltronException fun SemanticsNodeInteraction.getDescription() = this.getProperty("selector")?.description +fun SemanticsNodeInteractionCollection.getTestContext() = this.getProperty("testContext") + ?: throw UltronException("Couldn't get testContext from $this") + +fun SemanticsNodeInteractionCollection.getSemanticsSelector() = this.getProperty("selector") + ?: throw UltronException("Couldn't get selector from $this") + +fun SemanticsNodeInteraction.getTestContext() = this.getProperty("testContext") + ?: throw UltronException("Couldn't get testContext from $this") + +fun SemanticsNodeInteraction.getSemanticsSelector() = this.getProperty("selector") + ?: throw UltronException("Couldn't get selector from $this") + fun SemanticsNodeInteraction.getConfigField(name: String): Any? { for ((key, value) in this.fetchSemanticsNode().config) { if (key.name == name) { @@ -34,4 +50,16 @@ fun SemanticsNodeInteraction.requireSemantics( val msg = "${errorMessage()}, the node is missing [${missingProperties.joinToString()}]" throw AssertionError(msg) } -} \ No newline at end of file +} + +fun SemanticsNodeInteraction.findNodeInTree( + matcher: SemanticsMatcher, + useUnmergedTree: Boolean, +): SemanticsNodeInteraction { + return SemanticsNodeInteraction( + testContext = this.getTestContext(), + useUnmergedTree = useUnmergedTree, + selector = this.getSemanticsSelector().addFindNodeInTreeSelector("findNodeInTree", matcher) + ) +} + diff --git a/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsSelectorExt.kt b/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsSelectorExt.kt new file mode 100644 index 00000000..f861baf6 --- /dev/null +++ b/ultron-compose/src/main/java/com/atiurin/ultron/extensions/SemanticsSelectorExt.kt @@ -0,0 +1,19 @@ +package com.atiurin.ultron.extensions + +import androidx.compose.ui.test.SelectionResult +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsSelector + +fun SemanticsSelector.addFindNodeInTreeSelector( + selectorName: String, + matcher: SemanticsMatcher +): SemanticsSelector { + return SemanticsSelector( + "(${this.description}).$selectorName(${matcher.description})", + requiresExactlyOneNode = false, + chainedInputSelector = this + ) { nodes -> + SelectionResult(nodes.findNodeInTree(matcher)) + } +} + diff --git a/ultron/src/main/java/com/atiurin/ultron/core/espresso/recyclerview/UltronRecyclerViewItem.kt b/ultron/src/main/java/com/atiurin/ultron/core/espresso/recyclerview/UltronRecyclerViewItem.kt index 2cd90156..ec6560c3 100644 --- a/ultron/src/main/java/com/atiurin/ultron/core/espresso/recyclerview/UltronRecyclerViewItem.kt +++ b/ultron/src/main/java/com/atiurin/ultron/core/espresso/recyclerview/UltronRecyclerViewItem.kt @@ -26,6 +26,10 @@ open class UltronRecyclerViewItem { */ protected constructor() + fun child(block: () -> Matcher): Lazy> = lazy { + getChild(block()) + } + constructor( ultronRecyclerView: UltronRecyclerView, itemViewMatcher: Matcher,