Tester la navigation au clavier

L'automatisation des tests de navigation au clavier permet de s'assurer que votre application offre une expérience utilisateur cohérente et prévisible sur les claviers matériels, les pavés directionnels et les outils d'accessibilité. Compose fournit des API de test intégrées pour simuler la saisie au clavier et vérifier les états de focus.

Configurer des tests de focus

Pour écrire des tests de focus, utilisez createComposeRule() à partir de la bibliothèque de test Compose :

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

Vérifier les cibles de sélection

Vous pouvez vérifier si un élément d'interface utilisateur peut recevoir le focus en effectuant un clic ou en demandant le focus, puis en affirmant son état à l'aide de assertIsFocused() et 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()
}

Tester le balayage du focus unidimensionnel

Simulez les frappes sur les touches Tabulation et Maj+Tabulation à l'aide de performKeyInput pour vérifier que le focus progresse dans l'UI dans l'ordre d'apparence. Nous vous recommandons de vérifier la navigation vers l'avant (Key.Tab) et vers l'arrière (Key.Tab en maintenant la touche Key.ShiftLeft enfoncée) pour vous assurer de l'intégrité de la traversée bidirectionnelle :

@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()
}

Tester le parcours directionnel bidimensionnel

Simulez les touches fléchées ou les touches directionnelles du pavé directionnel à l'aide de Key.DirectionDown, Key.DirectionUp, Key.DirectionRight et 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()
}

Tester le comportement de sélection du champ de texte

Vérifiez que les champs de texte sur une seule ligne permettent de passer au champ suivant avec la touche Tabulation, tandis que les champs de texte multilignes conservent la sélection :

@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()
}