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():
clickOnTextclickOnTagclickOnContentDescriptionclickOnRootView
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:
clickOnTextclickOnTagclickOnContentDescriptionclickOnRootView
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
Componentis not being rendered if there is no click on theRow.- In order to do this, find the
Componentbased 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
LithoViewwith the help ofLithoTestRule.act{}.- Clicking on the content description of the row triggers the state update.
- Confirm that the
Componentis 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.