kakaocup / compose Goto Github PK
View Code? Open in Web Editor NEWNice and simple DSL for Espresso Compose UI testing in Kotlin
Home Page: https://kakaocup.github.io/Compose/
License: Apache License 2.0
Nice and simple DSL for Espresso Compose UI testing in Kotlin
Home Page: https://kakaocup.github.io/Compose/
License: Apache License 2.0
In my testing if I provide ComposeScreen with viewBuilderAction = { hasTestTag(C.Screen.main_screen) }
it doesn't work. I tested a bit, and I suspect that it uses wrong hierarchy matchers. If I do
composeTestRule.onNode(
hasTestTag(C.Tag.profile_auth_button).and(
hasParent(
hasTestTag(C.Screen.profile_screen)
)
)
).performClick()
``` then it also doesn't work, but if I change hasParent for hasAnyAncestors it does work.
Maybe I am doing something wrong? My initial attempt was like this
onComposeScreen(composeTestRule) {
authButton {
assertIsDisplayed()
performClick()
}
}
The current version does not have the ability to check the length of a lazy list
Thanks for releasing this feature, I have been experimenting with Kakao and have successfully used it for Espresso and Espresso web views, and now I'm testing the Compose views. The app I'm testing contains all these components.
The problem I'm having is that I cannot get a testTag to match, even though it works fine with raw Compose [ like this: composeTestRule.onNode(androidx.compose.ui.test.hasTestTag(“MyStatusArea")).assertIsDisplayed() ]
I was hoping that you might spot some issue in my tree log. I'm a newbie to Compose, but have been successful in using the raw Compose functions.
Kind Regards,
Pentti
This is the Exception I get when using Kakao Compose:
java.lang.AssertionError: Failed to perform isDisplayed check.
Can't retrieve node at index '0' of '((isRoot).children).filter(TestTag = 'MyStatusArea')'
There are no existing nodes for that selector.
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrDie(SemanticsNodeInteraction.kt:169)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNode(SemanticsNodeInteraction.kt:106)
at androidx.compose.ui.test.AndroidAssertions_androidKt.checkIsDisplayed(AndroidAssertions.android.kt:29)
at androidx.compose.ui.test.AssertionsKt.assertIsDisplayed(Assertions.kt:33)
at io.github.kakaocup.compose.node.NodeAssertions$DefaultImpls.assertIsDisplayed(NodeAssertions.kt:11)
at io.github.kakaocup.compose.node.KNode.assertIsDisplayed(KNode.kt:7)
I've followed the sample and have this in my page object:
val statusArea = KNode(this) {
hasTestTag(“MyStatusArea")
}
And my test scenario contains:
onComposeScreen(composeTestRule) {
statusArea {
assertIsDisplayed()
}
}
I've logged the tree using this command:
composeTestRule.onRoot(useUnmergedTree = true).printToLog("COMPOSE_LOG")
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: printToLog:
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: Printing with useUnmergedTree = 'true'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: Node #1 at (l=0.0, t=171.0, r=1080.0, b=2148.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #2 at (l=0.0, t=171.0, r=1080.0, b=2148.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: VerticalScrollAxisRange = 'androidx.compose.ui.semantics.ScrollAxisRange@db65b2a'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: Actions = [ScrollBy]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #3 at (l=0.0, t=699.0, r=1080.0, b=699.0)px, Tag: 'MyCircleProgress'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #4 at (l=342.0, t=501.0, r=738.0, b=897.0)px, Tag: 'MyButton'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Role = 'Button'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | [Disabled]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Actions = [OnClick]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | MergeDescendants = 'true'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #6 at (l=447.0, t=626.0, r=633.0, b=685.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Text = ‘[Waiting]’
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Actions = [GetTextLayoutResult]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #8 at (l=471.0, t=685.0, r=610.0, b=773.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Text = '[1.9%]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Actions = [GetTextLayoutResult]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #1000000004 at (l=0.0, t=0.0, r=0.0, b=0.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Role = 'Button'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #10 at (l=427.0, t=985.0, r=654.0, b=1084.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Role = 'Button'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Actions = [OnClick]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | MergeDescendants = 'true'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #11 at (l=471.0, t=1009.0, r=610.0, b=1061.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Text = '[Cancel]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Actions = [GetTextLayoutResult]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #1000000010 at (l=0.0, t=0.0, r=0.0, b=0.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Role = 'Button'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #13 at (l=0.0, t=1128.0, r=1080.0, b=1386.0)px, Tag: 'MyStatusArea'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #14 at (l=0.0, t=1128.0, r=540.0, b=1386.0)px, Tag: 'MyStatsNumberWithTitleBox'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | |-Node #15 at (l=254.0, t=1172.0, r=287.0, b=1246.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | | Text = '[4]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | | Actions = [GetTextLayoutResult]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | |-Node #17 at (l=151.0, t=1290.0, r=389.0, b=1342.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Text = '[Files checked]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Actions = [GetTextLayoutResult]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #19 at (l=540.0, t=1128.0, r=1080.0, b=1386.0)px, Tag: 'MyStatsNumberWithTitleBox'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #20 at (l=794.0, t=1172.0, r=827.0, b=1246.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Text = '[0]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | | Actions = [GetTextLayoutResult]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | |-Node #22 at (l=687.0, t=1290.0, r=933.0, b=1342.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Text = '[Apps checked]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Actions = [GetTextLayoutResult]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #24 at (l=0.0, t=1386.0, r=1080.0, b=1562.0)px, Tag: 'MyHistoryCard'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: Actions = [OnClick]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: MergeDescendants = 'true'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #27 at (l=55.0, t=1430.0, r=143.0, b=1518.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | ContentDescription = '[]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Role = 'Image'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #28 at (l=198.0, t=1445.0, r=445.0, b=1504.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Text = ‘[My history]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: | Actions = [GetTextLayoutResult]
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: |-Node #30 at (l=992.0, t=1455.0, r=1014.0, b=1494.0)px
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: ContentDescription = '[]'
10-16 19:04:10.055 1595 1730 D COMPOSE_LOG: Role = 'Image'
I want to get all items from Row or Column and cast them to specific PageObject. But now you can use only the Compose testing API to get all semantic nodes:
private val semanticItems = semanticsProvider
.onAllNodes(hasTestTag(TestTag.MoneyItem.Title), useUnmergedTree = true)
.fetchSemanticsNodes()
val items: List<KDashboardMoneyItemNode> = semanticItems.mapIndexed { index, _ ->
child {
useUnmergedTree = true
hasTestTag(TestTag.MoneyItem.Tag)
hasPosition(index)
}
}
This solution has many disadvantages. It would be better if Kakao added children
function to get all children nodes to hide low level api.
Hello.
All assertions in the "NodeAssertions" interface are use the "check()" function, which has a "description" parameter.
But this parameter has not been added to the assert functions in interface "NodeAssertions"
I would like to modify assert functions, for example:
fun assertTextContains(
assertDescription: String? = null,
value: String,
substring: Boolean = false,
ignoreCase: Boolean = false
) {
delegate.check(description = assertDescription, ComposeBaseAssertionType.ASSERT_TEXT_CONTAINS) { assertTextContains(value, substring, ignoreCase) }
}
A KLazyListItemNode can have children like title and subtitle text, but can't to verify title node and subtitle node because nodes are merged.
Workaround: set useUnmergedTree is true
class KItem(
semanticsNode: SemanticsNode,
semanticsProvider: SemanticsNodeInteractionsProvider,
): KLazyListItemNode<KItem>(semanticsNode, semanticsProvider) {
val title: KNode = child {
useUnmergedTree = true // add this
hasTestTag("title")
}
val subtitle: KNode = child {
useUnmergedTree = true // add this
hasTestTag("subtitle")
}
}
Is there a way to make this code easier to use?
ComposeTestRule
can provide functionality for awaiting views what can be useful for many test cases. However code below don't looks great to me.
fun BaseNode<*>.waitFor(composeTestRule: ComposeTestRule, timeoutMillis: Long = 1_000) {
composeTestRule.waitUntil(timeoutMillis) {
try {
this.delegate.interaction.semanticsNodeInteraction.assertExists()
true
} catch (e: AssertionError) {
false
}
}
}
Want to find a way how to do the same way but without composeTestRule reinjection to the function
At the end wanna have something like
ComposeScreen.onComposeScreen<MyScreen>(composeTestRule) {
waitFor()
heading {
waitFor()
hasText(it)
}
}
Compared to View based DSL, compose withText, hasContentDescriptionExactly e.t.c. are missing overload which accepts R.string ids. It may be cool to have them with compose too
Hi, I have a problem with matching child nodes for the ComposeScreen
. It works in simple cases but for the current screen it won’t work:
fun SimpleScreen() {
Scaffold(Modifier.testTag(TestTag.SimpleScreen.Tag)) {
Card {
Text(
"TestTitle",
Modifier
.padding(16.dp)
.testTag(TestTag.SimpleScreen.Title)
)
}
}
}
The following PageObject can’t find our title at this screen:
class KSimpleScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen<KSimpleScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTag.SimpleScreen.Tag) }
) {
val title: KNode = child {
hasTestTag(TestTag.SimpleScreen.Title)
}
}
It can be fixed by deleting child
and using hasAnyAncestor
matcher:
val title: KNode = KNode(semanticsProvider) {
hasAnyAncestor((androidx.compose.ui.test.hasTestTag(TestTag.SimpleScreen.Tag)))
hasTestTag(TestTag.SimpleScreen.Title)
}
But it’s not convenient and it would be great if BaseNode had this matcher by default for the parent node or allowed to change this behavior.
Hi,
First of all, thank you very much for the library!
I'm wondering, how can I test a material3 DatePicker that I'm using in a Compose screen? I can't see any instruction about how to define it nor how to initialize it in Compose.
The most I could do is:
private val datePickerNode = KNode(semanticsProvider) {
hasTestTag(DATE_PICKER_TAG)
}
but this returns a KNode
with which I can't do anything related to a date picker.
Thanks!
Hi,
I would like to ask for api to check for absence of items in the KLazyListNode.
We don’t have any possibility to test Lazy lists (LazyColumn/LazyRow). Even if we use compose testing API to get all semantic nodes it gets only visible items:
val semanticNodes = provider
.onNode(matcher)
.onChildren()
.fetchSemanticsNodes() // Gets only visible items in LazyColumn/LazyRow
It would be great if Kakao Compose added a similar API like in KRecyclerView to get item by index and scroll to specific list’s item.
I need to find not merged element in compose tree.
So how can I set useUnmergedTree to true to use in ViewBuilder?
When I use performGesture
on the KNode
, I get deprecation warnings for any method from the GestureScope
like this:
Replaced by TouchInjectionScope. Use `performTouchInput` instead of `performGesture`
It seems to originate from compose-junit
Something like this has to be present in NodeActions, since performGesture is deprecated in compose
fun performTouchInput(
block: TouchInjectionScope.() -> Unit
) {
delegate.perform(object : ComposeOperationType {
override val name: String = "performTouchInputAction"
}) { performTouchInput(block) }
}
#54
this change introduced java 17 as build jdk. but there is no need to update source and binary compability to java 17 at the moment. this will force users to match java target.
addind java and kotlin targets for java 8 or 11 could be enough.
Hi
There is a problem with LazyColumn
and clickable
modifier, if I add clickable
to item of LazyColumn
then KLazyListItemNode
can't find child.
For example I modified your sample and tried to run
@Composable
private fun ListItemHeader(item: LazyListItem.Header, modifier: Modifier = Modifier) {
Box(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.clickable { }
.clip(RoundedCornerShape(8.dp))
.background(Color.Green)
.then(modifier)
) {
Text(
item.title,
Modifier
.padding(8.dp)
.testTag("LazyListHeaderTitle")
)
}
}
I got error
java.lang.AssertionError: Failed to assert the following: (Text + EditableText = [Items from 1 to 10])
Can't retrieve node at index '0' of '(hasAnyAncestorThat(Semantics node id = 18)) && (TestTag = 'LazyListHeaderTitle')'
There are no existing nodes for that selector.
Hi,`
I'd like to perform some custom assertions on a field such as the one in the example code below. I'm sorry if this is an obvious question, but I've been unsuccessful in my attempts to assign a variable to the contents of the "testNumber" field.
private val testNumber: KNode = onNode {
hasTestTag("test_number")
hasPosition(0)
useUnmergedTree = true
}
testNumber {
assertIsDisplayed()
assertTextEquals(number)
}
Many thanks,
Pentti
`
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.