Giter Site home page Giter Site logo

kakaocup / compose Goto Github PK

View Code? Open in Web Editor NEW
122.0 5.0 12.0 1.15 MB

Nice and simple DSL for Espresso Compose UI testing in Kotlin

Home Page: https://kakaocup.github.io/Compose/

License: Apache License 2.0

Kotlin 100.00%
hacktoberfest android kotlin dsl espresso compose android-testing ui-testing testing-framework testing-library

compose's Introduction

Kakao Compose

Kotlin version badge Telegram Telegram

Nice and simple DSL for Espresso Compose in Kotlin

coco

Benefits

  • Readability
  • Reusability
  • Extensible DSL
  • Interceptors

Concept

The one of the main concepts of Jetpack Compose is a presentation of UI according to UI tree (UI hierarchy) approach with a parent-children relationships support. It means that the related UI test library has to support a parent-children relationships for Nodes by default. It will be discovered below how Kakao Compose library supports mentioned parent-children relationships approach.

How to use it

Create Screen

Create your entity ComposeScreen where you will add the views involved in the interactions of the tests:

class MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
    ComposeScreen<MainActivityScreen>(
        semanticsProvider = semanticsProvider
    )

ComposeScreen can represent the whole user interface or a portion of UI. If you are using Page Object pattern you can put the interactions of Kakao inside the Page Objects.

Described way of Screen definition is very similar with the way that Kakao library offers. But, usually, Screen in Jetpack Compose is a UI element (Node) too. That's why there is an additional option to declare ComposeScreen:

class MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
    ComposeScreen<MainActivityScreen>(
        semanticsProvider = semanticsProvider,
        viewBuilderAction = { hasTestTag("MainScreen") }
    )

So, ComposeScreen is a BaseNode's inheritor in Kakao-Compose library. And, as you've seen above, there is a possibility to describe ComposeScreen without mandatory viewBuilderAction in cases when Screen is an abstraction without clear connection with any Node.

Create KNode

ComposeScreen contains KNode, these are the Jetpack Compose nodes where you want to do the interactions:

class MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
    ComposeScreen<MainActivityScreen>(
        semanticsProvider = semanticsProvider,
        viewBuilderAction = { hasTestTag("MainScreen") }
    ) {

    val myButton: KNode = child {
        hasTestTag("myTestButton")
    }
}

myButton was declared as a child of MainActivityScreen. It means that myButton will be calculated using matchers specified in a lambda explicitly and a parent matcher implicitly (MainActivityScreen). Under the hood, the SemanticMatcher of myButton is equal to hasTestTag("myTestButton") + hasParent(MainActivityScreen.matcher).

Also, KNode can be declared as a child of another KNode:

class MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
    ComposeScreen<MainActivityScreen>(
        semanticsProvider = semanticsProvider,
        viewBuilderAction = { hasTestTag("MainScreen") }
    ) {

    val myButton: KNode = child {
        hasTestTag("myTestButton")
    }

    val myButton2: KNode = myButton.child {
        hasTestTag("myTestButton2")
    }
}

myButton2 will be calculated with the following SemanticMatcher = hasTestTag("myTestButton") + hasParent(myButton.matcher) + hasParent(MainActivityScreen.matcher). But, we advise not to abuse inheritance and use only the following chain: "ComposeScreen" - "Element of ComposeScreen".

The last, KNode can be declared without child function using only explicit matchers:

class MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
    ComposeScreen<MainActivityScreen>(
        semanticsProvider = semanticsProvider,
        viewBuilderAction = { hasTestTag("MainScreen") }
    ) {

    val myButton = KNode(this) {
        hasTestTag("myTestButton")
    }
}

Every KNode contains many matches. Some examples of matchers provided by Kakao Compose:

  • hasText
  • hasTestTag
  • and more

Like in Espresso you can combine different matchers:

val myButton = KNode(this) {
    hasTestTag("myTestButton")
    hasText("Button 1")
}

Write the interaction.

The syntax of the test with Kakao is very easy, once you have the ComposeScreen and the KNode defined, you only have to apply the actions or assertions like in Espresso:

class ExampleInstrumentedTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun simpleTest() {
        onComposeScreen<MainActivityScreen>(composeTestRule) {
            myButton {
                assertIsDisplayed()
                assertTextContains("Button 1")
            }

            onNode {
                hasTestTag("doesNotExist")
            }.invoke {
                assertDoesNotExist()
            }
        }
    }
}

Lazy lists testing

⚠️ This API is experimental and might change in future!

To test lazy lists such as LazyRow or LazyColumn you should add KLazyListNode into your ComposeScreen:

val list = KLazyListNode(
    semanticsProvider = semanticsProvider,
    viewBuilderAction = { hasTestTag("LazyList") },
    itemTypeBuilder = {
        itemType(::LazyListItemNode)
        itemType(::LazyListHeaderNode)
    },
    positionMatcher = { position -> SemanticsMatcher.expectValue(LazyListItemPosition, position) }
)

Inside itemTypeBuilder function you should register KLazyListItemNode types to differentiate elements in lazy list:

class LazyListItemNode(
    semanticsNode: SemanticsNode,
    semanticsProvider: SemanticsNodeInteractionsProvider,
) : KLazyListItemNode<LazyListItemNode>(semanticsNode, semanticsProvider)

class LazyListHeaderNode(
    semanticsNode: SemanticsNode,
    semanticsProvider: SemanticsNodeInteractionsProvider,
) : KLazyListItemNode<LazyListHeaderNode>(semanticsNode, semanticsProvider) {
    val title: KNode = child {
        hasTestTag("LazyListHeaderTitle")
    }
}

The element position might be changed during the scroll due to lazy list construction, that’s why we should provide positionMatcher to determine the element position correctly. It could be achieved in different ways, for example you can determine item position through TestTag:

LazyColumn(
    Modifier
        .fillMaxSize()
        .testTag("LazyList")
) {
    itemsIndexed(items) { index, item ->
        when (item) {
            is LazyListItem.Header -> ListItemHeader(item, Modifier.testTag("position=$index"))
            is LazyListItem.Item -> ListItemCell(item, Modifier.testTag("position=$index"))
        }
    }
}

And then check this position inside positionMatcher lambda:

positionMatcher = { position -> hasTestTag("position=$position") }

But it will be more convenient and less error prone to create custom semantics property and custom modifier:

val LazyListItemPosition = SemanticsPropertyKey<Int>("LazyListItemPosition")
var SemanticsPropertyReceiver.lazyListItemPosition by LazyListItemPosition

fun Modifier.lazyListItemPosition(position: Int): Modifier {
    return semantics { lazyListItemPosition = position }
}

And check an item position with SemanticsMatcher:

positionMatcher = { position -> SemanticsMatcher.expectValue(LazyListItemPosition, position) }

So the typical lazy list test may look like this:

 @Test
fun lazyListTest() {
    onComposeScreen<LazyListScreen>(composeTestRule) {
        list {
            firstChild<LazyListHeaderNode> {
                title.assertTextEquals("Items from 1 to 10")
            }
            childWith<LazyListItemNode> {
                hasText("Item 1")
            } perform {
                assertTextEquals("Item 1")
            }
            childAt<LazyListItemNode>(10) {
                assertTextEquals("Item 10")
            }
        }
    }
}

Check the lazy list test example for more information.

Intercepting

If you need to add custom logic during the Kakao-Compose -> Espresso(Compose) call chain (for example, logging) or if you need to completely change the events/commands that are being sent to Espresso during runtime in some cases, you can use the intercepting mechanism.

Interceptors are lambdas that you pass to a configuration DSL that will be invoked before calls happening from inside Kakao-Compose.

You have the ability to provide interceptors at two main different levels: Kakao-Compose runtime and any individual BaseNode instance. Interceptors in Kakao-Compose support a parent-children concept too. It means that any BaseNode inherits interceptors of his parents.

On each invocation of Espresso function that can be intercepted, Kakao-Compose will aggregate all available interceptors for this particular call and invoke them in descending order: Active BaseNode interceptor -> Interceptor of the parent Active BaseNode -> ... -> Kakao-Compose interceptor.

Each of the interceptors in the chain can break the chain call by setting isOverride to true during configuration. In that case Kakao-Compose will not only stop invoking remaining interceptors in the chain, but will not perform the Espresso call. It means that in such case, the responsibility to actually invoke Espresso lies on the shoulders of the developer.

Here's the examples of intercepting configurations:

class SomeTest {
    @Before
    fun setup() {
        KakaoCompose { // KakaoCompose runtime
            intercept {
                onComposeInteraction {
                    onAll { list.add("ALL") }
                    onCheck { _, _ -> list.add("CHECK") }
                    onPerform { _, _ -> list.add("PERFORM") }
                }
            }
        }
    }

    @Test
    fun test() {
        onComposeScreen<MyScreen> {
            intercept {
                onCheck { interaction, assertion -> // Intercept check() call
                    Log.d("KAKAO", "$interaction is checking $assertion")
                }
            }

            myView {
                intercept { // Intercepting ComposeInteraction calls on this individual Node
                    onPerform(true) { interaction, action -> // Intercept perform() call and overriding the chain
                        // When performing actions on this view, Kakao level interceptor will not be called
                        // and we have to manually call Espresso now.
                        Log.d("KAKAO_NODE", "$interaction is performing $action")
                        interaction.perform(action)
                    }
                }
            }
        }
    }
}

For more detailed info please refer to the documentation.

Setup

Maven

<dependency>
    <groupId>io.github.kakaocup</groupId>
    <artifactId>compose</artifactId>
    <version><latest version></version>
    <type>pom</type>
</dependency>

or Gradle:

dependencies {
    androidTestImplementation 'io.github.kakaocup:compose:<latest version>'
}

Contribution Policy

Kakao Compose is an open source project, and depends on its users to improve it. We are more than happy to find you interested in taking the project forward.

Kindly refer to the Contribution Guidelines for detailed information.

Code of Conduct

Please refer to Code of Conduct document.

License

Kakao Compose is open source and available under the Apache License, Version 2.0.

Thanks for supporting Open Source

compose's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

compose's Issues

Add API to get children from specific node

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.

viewBuilderAction doesn't see ancestor nodes

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()
}
}

performGesture is deprecated

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

Question on how I can retrieve the actual value of a field for some custom assertions

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

`

0.3 version requeries java 17

#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.

DatePicker Compose testing

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!

Problems with matching child nodes for the Compose Screen

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.

Add support for wait operation

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)
      }
}

Can not assert child of KLazyListItemNode

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?

Problem with LazyColumn and clickable modifier

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.

Add R.string support for hasText and such

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

NodeActions.performTouchInput

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) }
}

Add API to test Lazy lists

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.

Enhancement - add description parameter for NodeAssertions

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) }
    }

Снимок экрана 2023-03-31 в 20 26 34

Failed to perform isDisplayed check

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'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.