Cómo probar la navegación del enfoque

Automatizar las pruebas de navegación con enfoque garantiza que tu app proporcione una experiencia del usuario coherente y predecible en teclados de hardware, pads direccionales y herramientas de accesibilidad. Compose proporciona APIs de prueba integradas para simular la entrada de teclas y verificar los estados de enfoque.

Cómo configurar pruebas de enfoque

Para escribir pruebas de enfoque, usa createComposeRule() de la biblioteca de pruebas de Compose:

@RunWith(AndroidJUnit4::class)
class FocusNavigationTest {
    @get:Rule
    val composeTestRule = createComposeRule()

Verifica los objetivos de enfoque

Puedes verificar si un elemento de la IU puede recibir el enfoque haciendo clic en él o solicitando el enfoque, y, luego, afirmando su estado con assertIsFocused() y assertIsNotFocused().

@Test
fun interactiveElement_isFocusTarget() {
    composeTestRule.setContent {
        AppTheme {
            CardListScreen()
        }
    }

    val firstCard = composeTestRule.onNodeWithTag("card_1")
    val secondCard = composeTestRule.onNodeWithTag("card_2")

    // Focus the first element
    firstCard.performClick()
    firstCard.assertIsFocused()
    secondCard.assertIsNotFocused()
}

Cómo probar el recorrido del enfoque unidimensional

Simula las pulsaciones de las teclas Tab y Mayúsculas + Tab con performKeyInput para verificar que el enfoque avanza por la IU en el orden de apariencia. Como práctica recomendada, verifica la navegación hacia adelante (Key.Tab) y la navegación hacia atrás (Key.Tab con Key.ShiftLeft presionado) para garantizar la integridad del recorrido bidireccional:

@Test
fun tabKey_navigatesInAppearanceOrder() {
    composeTestRule.setContent {
        AppTheme {
            CardListScreen()
        }
    }

    val firstCard = composeTestRule.onNodeWithTag("card_1")
    val secondCard = composeTestRule.onNodeWithTag("card_2")
    val thirdCard = composeTestRule.onNodeWithTag("card_3")

    firstCard.performClick()
    firstCard.assertIsFocused()

    // Press Tab -> Moves to second card
    firstCard.performKeyInput {
        pressKey(Key.Tab)
    }
    secondCard.assertIsFocused()
    firstCard.assertIsNotFocused()

    // Press Tab -> Moves to third card
    secondCard.performKeyInput {
        pressKey(Key.Tab)
    }
    thirdCard.assertIsFocused()

    // Press Shift + Tab -> Moves backward to second card
    thirdCard.performKeyInput {
        withKeyDown(Key.ShiftLeft) {
            pressKey(Key.Tab)
        }
    }
    secondCard.assertIsFocused()
}

Prueba el desplazamiento direccional bidimensional

Simula las teclas de flecha o las teclas direccionales del pad direccional con Key.DirectionDown, Key.DirectionUp, Key.DirectionRight y Key.DirectionLeft.

@Test
fun arrowKeys_moveFocusTwoDimensionallyWithoutWrap() {
    composeTestRule.setContent {
        AppTheme {
            GridLayoutScreen()
        }
    }

    val topLeftButton = composeTestRule.onNodeWithTag("btn_top_left")
    val bottomLeftButton = composeTestRule.onNodeWithTag("btn_bottom_left")

    topLeftButton.performClick()
    topLeftButton.assertIsFocused()

    // Down arrow moves focus downward to bottom-left button
    topLeftButton.performKeyInput {
        pressKey(Key.DirectionDown)
    }
    bottomLeftButton.assertIsFocused()

    // Directional keys do not wrap around: pressing Down on bottom element stays focused
    bottomLeftButton.performKeyInput {
        pressKey(Key.DirectionDown)
    }
    bottomLeftButton.assertIsFocused()
}

Prueba el comportamiento del enfoque del campo de texto

Verifica que los campos de texto de una sola línea avancen el enfoque con la tecla Tab, mientras que los campos de texto de varias líneas conserven el enfoque:

@Test
fun textField_handlesTabAccordingToLineLimits() {
    composeTestRule.setContent {
        AppTheme {
            FormScreen()
        }
    }

    val singleLineNode = composeTestRule.onNodeWithTag("single_line_field")
    val multiLineNode = composeTestRule.onNodeWithTag("multi_line_field")
    val submitButtonNode = composeTestRule.onNodeWithTag("submit_button")

    // Single-line field advances focus on Tab
    singleLineNode.performClick()
    singleLineNode.assertIsFocused()
    singleLineNode.performKeyInput { pressKey(Key.Tab) }
    multiLineNode.assertIsFocused()

    // Multi-line field keeps focus and inserts '\t'
    multiLineNode.performKeyInput { pressKey(Key.Tab) }
    multiLineNode.assertIsFocused()
    submitButtonNode.assertIsNotFocused()

    // Shift + Tab escapes multi-line field to previous target
    multiLineNode.performKeyInput {
        withKeyDown(Key.ShiftLeft) { pressKey(Key.Tab) }
    }
    singleLineNode.assertIsFocused()
}