Interactions
An Interaction
is defined as any type of action that a user can perform on Components
. A good example is touching or clicking a Button
.
act{InteractionsScope.() -> Unit}
β
In Litho
, the state of the Components
is updated asynchronously, off the Main Thread. Once the background operations are finished, they are posted to the Main Thread to update the Component
.
During tests, it's important to ensure that everything is in sync in the Main Thread and the Background Thread, as in a real-life use case. This is the role of the act{}
function, which removes the responsibility to use the Loopers and manage the thread synchronisation, as shown in the following snippet:
mLithoTestRule.act(testLithoView) { clickOnTag("test_view") }
Only one of the defined interactions from InteractionsScope
needs to be called, Litho takes care of the rest.
The following interactions are exposed via act()
:
clickOnText
clickOnTag
clickOnContentDescription
clickOnRootView
Interactions can be chained to invoke multiples in a given order:
lithoViewRule
.act { clickOnText("Menu") }
.act { clickOnText("File") }
.act { clickOnText("New") }
.act { clickOnText("New Project...") }
idle()
β
Use act()
for events that trigger async updates (such as clicks
). Otherwise, there are sometimes async events triggered by layout (for example visibility events, or when the state is immediately updated in a render call) that can be manually waited for to finish by calling: idle()
.
For example, it may be needed if a component defines a visibility event that triggers an async state update. In such a case, idle()
should be called after layout to make sure the update is reflected in the UI before making test assertions:
override fun ComponentScope.render(): Component? {
val visibilityEventCalled = useState { false }
stateRef = AtomicReference(visibilityEventCalled.value)
return Column(
style =
Style.width(10.dp).height(10.dp).onVisible { visibilityEventCalled.update(true) })
}
}
mLithoTestRule.render { TestComponent() }
assertThat(stateRef.get()).isEqualTo(false)
mLithoTestRule.idle()
assertThat(stateRef.get()).isEqualTo(true)
Interactions with LithoTestRuleβ
How to test a click actionβ
Four types of 'click' are supported:
clickOnText
clickOnTag
clickOnContentDescription
clickOnRootView
The TestComponentβ
To illustrate how to test a click action, the following TestComponent
shows/hides the Text after the click action is performed on a Row
:
class TestComponent : KComponent() {
override fun ComponentScope.render(): Component? {
val showText = useState { false }
return Row(
style =
Style.width(100.px)
.height(100.px)
.onClick { showText.update { isTextShowing -> !isTextShowing } }
.contentDescription("row")) {
if (showText.value) {
child(Text(text = "Text"))
}
}
}
}
Testing the TestComponent for a click actionβ
The test case performs three steps:
- Confirm that the
Component
is not being rendered if there is no click on theRow
.- In order to do this, find the
Component
based on the Text or its Class by using either:findViewWithTextOrNull(String)
orfindComponent(Class<out Component?>)
and assert that it's null.
- In order to do this, find the
- Perform an action on the
LithoView
with the help ofLithoTestRule.act{}
.- Clicking on the content description of the row triggers the state update.
- Confirm that the
Component
is being rendered and is not null.- Any of the available methods can be used to find the
Component
.
- Any of the available methods can be used to find the
This test case is satisfied with the assertions shown in the following snippet:
val testLithoView = mLithoTestRule.render { TestComponent() }
LithoStats.resetAllCounters()
/** Find [Component] based on the text or [Component] class */
assertThat(testLithoView.findViewWithTextOrNull("Text")).isNull()
assertThat(testLithoView.findComponent(Text::class)).isNull()
/** perform interaction defined in [LithoTestRule] */
mLithoTestRule.act(testLithoView) { clickOnContentDescription("row") }
/** check number of state updates */
assertThat(LithoStats.componentTriggeredAsyncStateUpdateCount).isEqualTo(1)
/** Find [Component] based on the text or [Component] class */
assertThat(testLithoView.findViewWithTextOrNull("Text")).isNotNull()
assertThat(testLithoView.findComponent(Text::class)).isNotNull()
How to test VisiblityEvent
β
In order to test VisiblityEvent
instead of using LithoTestRule.render(Component)
, separate the methods that are being called under the render call.
This enables the state of the component to be checked before and after the visibility event is triggered:
val testLithoView =
mLithoTestRule.createTestLithoView { TestComponent() }.attachToWindow().measure()
/** Before the onVisible is called */
assertThat(testLithoView.findComponent(InnerComponent::class)).isNull()
/** Layout component and idle, triggering visibility event and any async updates */
testLithoView.layout()
mLithoTestRule.idle()
/** After the onVisible is called */
assertThat(testLithoView).containsExactlyOne(InnerComponent::class)
If there is any background work happening in an onVisible
call, remember to call idle()
after layout()
.
How to test State Updateβ
If a state update needs to be tested, the best thing to do is to trigger the event that causes the state update, as the state of the Component
is not being exposed.
Have another look at the TestComponent snippet, in the clicking
section, where the State Update is triggered by the Click Action
.