בדיקת הניווט באמצעות המיקוד

אוטומציה של בדיקות הניווט באמצעות מקשי החיצים מבטיחה שהאפליקציה תספק חוויית משתמש עקבית וצפויה במקלדות חומרה, במקשי החיצים ובכלי נגישות. ‫Compose מספקת ממשקי API מובנים לבדיקה, כדי לדמות קלט של מקשים ולאמת מצבי מיקוד.

הגדרת בדיקות מיקוד

כדי לכתוב בדיקות מיקוד, משתמשים ב-createComposeRule() מספריית הבדיקות של Compose:

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

אימות יעדי המיקוד

כדי לבדוק אם רכיב בממשק המשתמש יכול לקבל מיקוד, מבצעים קליק או שולחים בקשה למיקוד, ואז בודקים את הסטטוס שלו באמצעות assertIsFocused() ו-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()
}

בדיקה של מעבר בין רכיבים במיקוד חד-ממדי

מדמים הקשה על המקשים Tab ו-Shift+Tab באמצעות performKeyInput כדי לוודא שהמיקוד עובר בין רכיבי ממשק המשתמש לפי סדר ההופעה שלהם. השיטה המומלצת היא לאמת גם את הניווט קדימה (Key.Tab) וגם את הניווט אחורה (Key.Tab תוך כדי לחיצה על Key.ShiftLeft) כדי לוודא שהמעבר דו-כיווני תקין:

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

בדיקת ניווט דו-ממדי

אפשר לדמות את מקשי החיצים או את מקשי הכיוון של משטח ה-D באמצעות Key.DirectionDown,‏ Key.DirectionUp,‏ Key.DirectionRight ו-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()
}

בדיקה של התנהגות המיקוד בשדה טקסט

מוודאים שבשדות טקסט עם שורה אחת, המיקוד עובר בלחיצה על Tab, ובשדות טקסט עם כמה שורות המיקוד נשאר:

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