UI Block
UI blocks are a powerful tool for describing and interacting with user interface elements. They allow you to define UI elements within the context of their parent blocks, rather than the entire screen, which makes tests more readable, maintainable, and reliable.
For example, consider a UI block that represents a user’s name and status. We can define this block once and reuse it across different screens.
We can describe this block and use it on different screens.
Supported: Compose (CMP & Android), Espresso, Espresso Web, UiAutomator (UiObject2)
Compose
Create a class that inherits from UltronComposeUiBlock
.
class ContactCard(blockMatcher: SemanticsMatcher, blockDescription: String)
: UltronComposeUiBlock(blockMatcher, blockDescription) {
val name = child(hasTestTag(contactNameTag)).withName("Name in '$blockDescription'")
val status = child(hasTestTag(contactStatusTag))
}
UltronComposeUiBlock
accepts two parameters:
blockMatcher
– describes how to locate this block in the Compose element tree. This is a required parameter and must always be provided.blockDescription
– a description of the block that clearly identifies the UI container. This parameter is optional, with a default value ofblockDescription = ""
.
Note: To describe child elements of a UI block, you need to use the child()
method.
As shown in the example above, we added a custom name to the name
field. If an error occurs, this name will appear in the description of the element we tried to interact with. We recommend including the value of blockDescription
in the element description. This provides better context about the specific element being checked (or any other operation performed).
The next step is to integrate the block into a screen.
object SomeComposeScreen : Screen<SomeComposeScreen>(){
val card = ContactCard(hasTestTag(contactCardTag), "SomeComposeScreen contact card")
fun assertContactCard(contact: Contact){
softAssertion {
card.name.assertTextEquals(contact.name)
card.status.assertTextEquals(contact.status)
}
}
}
As seen in SomeComposeScreen
, we no longer need to know how to locate name
and status
. It's enough to describe how to locate the parent UI block – ContactCard
.
In addition to individual UI elements, child blocks can also represent other UI blocks. To describe a child UI block, you can use one of the overloaded child
methods.
- In Multiplatform, only the method requiring an explicit approach to creating the child block is available.
- In Android, you can simplify this further using reflection.
Compose Multiplatform
class ProfileBlock(blockMatcher: SemanticsMatcher, blockDescription: String)
: UltronComposeUiBlock(blockMatcher, blockDescription) {
val card = child(
childMatcher = hasTestTag(contactCardTag),
uiBlockFactory = { updatedMatcher ->
ContactCard(
blockMatcher = updatedMatcher,
blockDescription = "Contact card '$blockDescription'"
)
}
)
}
This method offers greater flexibility for creating child UI blocks.
updatedMatcher
– an updated matcher used to locate the ContactCard
only within the ProfileBlock
.
Compose Android Only
Reflection capabilities in Android are more advanced than in Multiplatform, allowing for simpler descriptions of child UI blocks.
class ProfileBlock(blockMatcher: SemanticsMatcher, blockDescription: String)
: UltronComposeUiBlock(blockMatcher, blockDescription) {
val card = child(
ContactCard(
blockMatcher = hasTestTag(contactCardTag),
blockDescription = "Contact card '$blockDescription'"
)
)
}
There are limitations to using this method:
The class must meet the following conditions to be instantiated:
- It must not be a nested or inner class. It should be defined at the top level or as a file-level class.
- It must have one of the following constructors:
- A constructor with one parameter of type SemanticsMatcher.
- A constructor with two parameters:
blockMatcher
of type SemanticsMatcher andblockDescription
of type String.
We can use the ProfileBlock
on the screen.
object SomeComposeScreen : Screen<SomeComposeScreen>(){
val profile = ProfileBlock(hasTestTag(profileTag), "SomeComposeScreen profile card")
fun assertContactCardInProfile(contact: Contact){
softAssertion {
profile.card.name.assertTextEquals(contact.name)
profile.card.status.assertTextEquals(contact.status)
}
}
}
The UltronComposeUiBlock
class has a uiBlock
property, which facilitates proper interaction with block elements.
object SomeComposeScreen : Screen<SomeComposeScreen>(){
val profile = ProfileBlock(hasTestTag(profileTag), "SomeComposeScreen profile block")
fun assertProfileContactIsDisplayed(){
profile.card.uiBlock.assertIsDisplayed()
}
}
Espresso
Create a class that inherits from UltronEspressoUiBlock
class ContactCard(blockMatcher: Matcher<View>, blockDescription: String)
: UltronEspressoUiBlock(blockMatcher, blockDescription) {
val name = child(withId(R.id.name)).withName("Name in '$blockDescription'")
val status = child(withId(R.id.name))
}
Add the block to the screen.
object SomeEspressoScreen : Screen<SomeEspressoScreen>(){
val card = ContactCard(withId(R.id.card), "SomeComposeScreen contact card")
fun assertContactCard(contact: Contact){
softAssertion {
card.name.hasText(contact.name)
card.status.hasText(contact.status)
}
}
}
Using reflection simplifies the implementation of child UI blocks by automating instantiation under specific conditions.
Child UI block with reflection.
class ProfileBlock(blockMatcher: Matcher<View>, blockDescription: String)
: UltronEspressoUiBlock(blockMatcher, blockDescription) {
val card = child(
ContactCard(
blockMatcher = withId(R.id.contactCard),
blockDescription = "Contact card of '$blockDescription'"
)
)
}
Child UI block with factory method
class ProfileBlock(blockMatcher: Matcher<View>, blockDescription: String)
: UltronEspressoUiBlock(blockMatcher, blockDescription) {
val card = child(
childMatcher = withId(R.id.contactCard),
uiBlockFactory = { updatedMatcher ->
ContactCard(
blockMatcher = updatedMatcher,
blockDescription = "Contact card '$blockDescription'"
)
}
)
}
Define block on screen
object SomeEspressoScreen : Screen<SomeEspressoScreen>(){
val profile = ProfileBlock(withId(R.id.profileBlock), "SomeEspressoScreen profile block")
fun assertContactCardInProfile(contact: Contact){
softAssertion {
profile.uiBlock.isDisplayed()
profile.card.uiBlock.isDisplayed()
profile.card.name.hasText(contact.name)
profile.card.status.hasText(contact.status)
}
}
}
Espresso Web
Create a class that inherits from UltronWebElementUiBlock
class WebContactCard(blockElement: UltronWebElement, blockDescription: String)
: UltronWebElementUiBlock(blockElement, blockDescription){
val name = child(id("name")).withName("Name in '$blockDescription'")
val status = child(className("status"))
}
Add the block to the screen.
object SomeWebScreen : Screen<SomeWebScreen>(){
val card = WebContactCard(id("card"), "SomeWebScreen contact card")
fun assertContactCard(contact: Contact){
softAssertion {
card.name.hasText(contact.name)
card.status.hasText(contact.status)
}
}
}
Child UI block with reflection
class WebProfileBlock(blockMatcher: UltronWebElement, blockDescription: String)
: UltronWebElementUiBlock(blockMatcher, blockDescription) {
val card = child(
WebContactCard(
blockElement = id("card"),
blockDescription = "Contact card of '$blockDescription'"
)
)
}
Child UI block with factory method
class WebProfileBlock(blockMatcher: UltronWebElement, blockDescription: String)
: UltronWebElementUiBlock(blockMatcher, blockDescription) {
val card = child(
childMatcher = id("card"),
uiBlockFactory = { updatedElement ->
ContactCard(
blockElement = updatedElement,
blockDescription = "Contact card '$blockDescription'"
)
}
)
}
UiAutomator
Only UiObject2 is supported.
Create a class that inherits from UltronUiObject2UiBlock
class UiAutomatorContactCard(blockDesc: String, blockSelector: () -> BySelector)
: UltronUiObject2UiBlock(blockDesc, blockSelector){
val name = child(bySelector(R.id.name)).withName("Name in '$blockDesc'")
val status = child(By.desc("status content desc"))
}
Add the block to the screen.
object SomeUiAutomatorScreen : Screen<SomeUiAutomatorScreen>(){
val card = UiAutomatorContactCard(
blockDesc="SomeUiAutomatorScreen contact card",
blockSelector=bySelector(R.id.card)
)
fun assertContactCard(contact: Contact){
softAssertion {
card.name.hasText(contact.name)
card.status.hasText(contact.status)
}
}
}
Child UI block with reflection
class UiAutomatorProfileBlock(blockDesc: String, blockSelector: () -> BySelector)
: UltronUiObject2UiBlock(blockDesc, blockSelector){
val card = child(
UiAutomatorContactCard(
blockDesc = "Contact card of '$blockDesc'",
blockSelector = { bySelector(R.id.card) }
)
)
}
Child UI block with factory method
class UiAutomatorProfileBlock(blockDesc: String, blockSelector: () -> BySelector)
: UltronUiObject2UiBlock(blockDesc, blockSelector){
val card = child(
selector = bySelector(R.id.card),
description = "Contact card of '$desc'",
uiBlockFactory = { desc, selector ->
UiAutomatorContactCard(desc, selector)
}
)
}