Android Compose: how to verify that a switch is on or off inside a unit test
Asked Answered
F

3

8

I have a Switch in a composable:

Switch(
    checked = false,
    modifier = Modifier.testTag("mySwitch")
)

And I'm trying to verify it from a compose unit test:

composeTestRule.onAllNodesWithTag("mySwitch")
    .assertAll(isToggleable() and isOff())

However it fails with the following exception:

java.lang.AssertionError: Failed to assertAll((ToggleableState is defined) && (ToggleableState = 'Off'))
Found '1' nodes not matching:
1) Node #8 at (l=955.0, t=387.0, r=1054.0, b=450.0)px, Tag: 'switch'
Has 4 siblings
Selector used: 'TestTag = 'mySwitch''

Apparently the Switch is neither toggleable or "on/off-eable". I've checked also with assertIsToggleable and assertIsOff separately, and both fail.

I've verified that the Switch is visible for the UI state used in the test.

Why does my test fail? It should be possible to easily test a freaking Switch. A Switch is the very definition of "Toggleable". How could I test it then, should I use custom semantic properties?


DISCLAIMER: This question is not the same as this one. I want to verify the Switch state, not to click it (that I will try later)

Flatware answered 18/8, 2022 at 0:13 Comment(0)
F
7

My test code was correct after all. assertIs[Not]Toggleable, assertIsOff, assertIsOn, and assertAll(isToggleable()) should work on a Switch. But due to a bug (or weird undocumented feature) in Compose 1.2.1 a Switch is not marked as "Toggleable" unless you pass a callback as parameter.

I figured out this by inspecting the source code of Switch.kt. This is the part that causes the problem:

@Composable
    @OptIn(ExperimentalMaterialApi::class)
    fun Switch(
        checked: Boolean,
        onCheckedChange: ((Boolean) -> Unit)?,
        ...
    ) {
        ...
        val toggleableModifier =
            if (onCheckedChange != null) {
                Modifier.toggleable(
                    value = checked,
                    onValueChange = onCheckedChange,
                    enabled = enabled,
                    role = Role.Switch,
                    interactionSource = interactionSource,
                    indication = null
                )
            } else {
                Modifier
            }

As you can see, unless you pass the optional parameter onCheckedChange, the Switch is not made Toggleable. And that in turn will cause all the isToggleable-like assertions to throw an exception, and the same will happen with the isOn and isOff assertions since they also require the node to be "Toggleable".

So passing a dummy onCheckedChange lambda to the switch in the test code solved the problem:

Switch(checked=True,onCheckedChange={})
Flatware answered 18/8, 2022 at 11:37 Comment(3)
Can you please post the full code and test code example? I can't get it working to toggle the Switch from on to off in the test.Quartile
@Quartile I have updated the answer. Check the final line.Flatware
Thank you. I tried this without success. Can you please also post your test code?Quartile
A
-1

Test

You can use .assertIsOn() inside the test.

private val checked = mutableStateOf(false)

@Before
fun launchContent() {
    composeTestRule.setContent {
        SharedComponentsTheme {
            Switch(
                checked = checked.value,
                onCheckedChange = { checked.value = !checked.value }
            )
        }
    }
}

@Test
fun testSwitchOn() {
    checked.value = false
    composeTestRule.onNodeWithTag("SWITCH_TAG")
        .performTouchInput { click() }
        .assertIsOn()
}

Code

In the code you need to set the toggleableState to On or Off

import androidx.compose.ui.state.ToggleableState.On
import androidx.compose.ui.state.ToggleableState.Off
import androidx.compose.ui.semantics.Role.Switch

@Composable
fun Switch(
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
) {
    androidx.compose.material3.Switch(
        modifier = Modifier
            .clearAndSetSemantics {
                testTag = "SWITCH_TAG",
                role = Switch,
                toggleableState = if (checked) {
                    On
                } else {
                    Off
                }
            },
        checked = checked,
        onCheckedChange = { onCheckedChange(it) }
    )
}
Absorbefacient answered 9/9, 2022 at 12:31 Comment(1)
Yes we should be able to, but it doesnt work unless onCheckedChange is explicitly passed.Flatware
K
-1

Providing a non-null onCheckedChange lambda will prevent the parent composable from receiving click events.

To avoid this you can use the following instead:

@Composable
fun SemanticSwitch(
    checked: Boolean,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: SwitchColors = SwitchDefaults.colors()
) {
    Switch(
        modifier = modifier.semantics {
            toggleableState = if(checked) ToggleableState.On else ToggleableState.Off
        },
        checked = checked,
        onCheckedChange = null,
        enabled = enabled,
        interactionSource = interactionSource,
        colors = colors
    )
}

Or if you only want to apply a modifier:

@Composable  
fun Modifier.applyCheckedSemantics(checked: Boolean): Modifier = this.semantics { 
    toggleableState = if(checked) ToggleableState.On else ToggleableState.Off  
}

Usage:

Switch(  
    modifier = Modifier  
        .testTag("mySwitch")  
        .applyCheckedSemantics(switchChecked),  
    checked = switchChecked,  
    onCheckedChange = null  
)
Katti answered 13/5 at 9:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.