Accessibility Tests in Compose - Name, Role, Value

Reading time: about 6 minutes

Published

Accessibility Tests  in Compose - Name, Role, Value

When writing tests for your app, you should also consider testing for accessibility-related things. And I get it; it can be challenging to know where to start. So, I decided to write this blog post about how to test some accessibility aspects.

In this post, we will add some accessibility-related tests for three custom components constructed with the help of clickable, selectable, and toggleable modifiers. These components were built in a blog post I wrote: Improving Android Accessibility with Modifiers in Jetpack Compose.

What Are We Testing?

The tests we're writing verify that the components have names, roles, and values. But where does this group come from? The background is that Web Content Accessibility Guidelines (WCAG) has a success criterion, "Name, Role, Value", which ensures that every element has a programmatically determinable name and role. Also, states, properties, and values that users can change are programmatically changeable.

And now, if you wonder why I'm mentioning something named "Web," the WCAG is also used to determine the minimum level of accessibility for mobile apps as well, despite the name.

Name, in this case, means the accessible name—so, the textual representation of the element. It can be, for example, a button's text, an icon button's content description, a label for a switch, or similar. It's what anyone using a screen reader hears. Voice access users use it to activate interactive elements.

Role, on the other hand, is the role of the element. It can be, for example, a button - which tells the user that, hey, this is a button, and it should behave as a button. A role is a promise of how things should work, so if you add a role, be sure to add the correct interactions as well. However, roles are used less on Android than on the web.

Value can refer to an element's state, property, or value. The exact thing is different per element. For example, with a checkbox, the value tells if it is checked, or with an accordion, it's the state that tells if it's opened or closed.

In the next section, we'll examine concrete examples of how to test the "Name, Role, Value" success criterion for a couple of custom components mentioned in the intro.

Writing the Tests, an Example

As mentioned in the beginning, these tests are written for components for a blog post I've written previously. We'll look into how to test three components: A switch, a radio button group, and a clickable row.

As the components in the blog posts were simplified for the sake of an example, these tests are also streamlined. With production-grade code, you usually have a bit more sophisticated strategies for, for example, finding the components that are being tested.

Toggleable

The first component we're testing is a switch like in the picture:

A pink rectangle with round corners, and it has the text "Toggleable" at the start (left) side of the row and a switch toggle at the end (right side) of the row.

We want to test three things: First, we want to ensure the component has an accessible name (so, the label of the switch). Second, the role should be correct—it should be a toggleable component. Third, the value should be correct before and after toggling the switch, so whether the switch is on or off.

Let's write a test:

class ToggleableTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun hasRoleNameValue() {
        composeTestRule.setContent {
            ModifiersExampleTheme {
                ToggleableScreen()
            }
        }

        val toggleableElement = 
            composeTestRule.onNode(hasTestTag("accessible-toggle"))

        // Assert accessible name
        toggleableElement.assertTextEquals("Toggleable")
        // Assert role
        toggleableElement.assertIsToggleable()
        // Assert value
        toggleableElement.assertIsOff()
        toggleableElement.performClick()
        toggleableElement.assertIsOn()
    }
}

First, the test needs a setup, so we need things like the composeTestRule and setting the content. Then we get the testable component with a test tag accessible-toggle. Finally, we have the tests for name, role, and value.

The test for checking the name is straightforward: We want to ensure that the element's text content equals the word on the label. We can assert that with assertTextEquals. To test the role, we can use a useful assert function, assertIsToggleable. Finally, to check if the value (so, the checked state) is correct, we can also use the utility functions assertIsOff and assertIsOn and, for toggling the state, performClick.

Selectable

The next component we're testing is a radio button group, as seen in the picture:

Two rows with pink background, text on the left, and a radio button on the right. The first one is selected and has the text "Option A," and the second one is not and has the text "Option B"

For this component, we ensure that both of the options have a name (so, the labels "Option A" and "Option B"), role as selectable, and value if the item is selected.

The test for this component is:

class SelectableTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Before
    fun setup() {
        composeTestRule.setContent {
            ModifiersExampleTheme {
                SelectableScreen()
            }
        }
    }

    @Test
    fun hasRoleNameValue() {
        val selectableElements = 
            composeTestRule.onAllNodes(hasTestTag("accessible-selectable"))

        // Assert accessible name
        selectableElements[0].assertTextEquals("Option A")
        selectableElements[1].assertTextEquals("Option B")
        // Assert role
        selectableElements.assertAll(isSelectable())
        // Assert value
        selectableElements[0].assertIsSelected()
        selectableElements[1].performClick()
        selectableElements[0].assertIsNotSelected()
        selectableElements[1].assertIsSelected()
    }
}

The structure is very similar to the previous test; first, the setup, then getting the elements, and then asserting name, role, and value. We're using the same assertTextEquals to check the elements' labels (so, names). Similarly to the toggleable, there are functions for asserting the role and values for the selectable: isSelectable(), assertIsSelected(), and .assertIsNotSelected().

Clickable

The final custom component for this blog post is a custom button that can be used to bookmark an item:

A pink rectangle with rounded corners, which has the text "Bookmark this item" on the left side, and at the end (right side), there is an outlined icon representing a bookmark.

We want to ensure that it has a name (so, the text "Bookmark this item"), the role of a button, and a state that communicates whether the item is bookmarked or not.

The following tests ensure that:

class ClickableTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun hasRoleNameValue() {
        composeTestRule.setContent {
            ModifiersExampleTheme {
                ClickableScreen()
            }
        }

        val clickableElement = 
            composeTestRule.onNode(hasTestTag("clickable"))

        // Assert accessible name
        clickableElement.assertTextEquals("Bookmark this item")

        // Assert role
        clickableElement.assert(
            SemanticsMatcher("has correct role") {
                it.config.getOrNull(SemanticsProperties.Role) == Role.Button
            },
        )

        // Assert state description
        clickableElement.assertStateDescription("Not bookmarked")
        clickableElement.performClick()
        clickableElement.assertStateDescription("Bookmarked")
    }

    private fun SemanticsNodeInteraction.assertStateDescription(
        stateDescription: String
    ) =
        assert(
            SemanticsMatcher("has correct state description") {
                it.config.getOrNull(SemanticsProperties.StateDescription) == stateDescription
            },
        )
}

Again, the setup and checking of the name are similar to the other two components. But to check if the component has a role of the button, we need to use a SemanticsMatcher.

SemanticMatcer is a wrapper for matching semantic nodes. We want to ensure that the element's semantic property Role matches Role.Button. We can do it by wrapping our check with a SemanticMatcher, and getting the element's SemanticProperties.Role from the element with it.config.getOrNull(SemanticsProperties.Role) and checking its value.

The same pattern works for testing the element's state description. To avoid code duplication, I've created an extension function, assertStateDescription, which is used to check the state description of the element.

Wrapping Up

In this blog post, we've discussed writing accessibility tests for the WCAG success criteria 4.1.2: Name, Role, Value. While they're not always relevant to mobile accessibility, this blog post aims to give an example of how to write accessibility tests.

Have you written tests for accessibility on Android? Please, share what you've learned!

Links in the Blog Post