Kiểm thử thao tác điều hướng bằng tiêu điểm

Việc tự động hoá các kiểm thử điều hướng tiêu điểm đảm bảo ứng dụng của bạn mang lại trải nghiệm người dùng nhất quán và dễ đoán trên bàn phím phần cứng, D-pad và các công cụ hỗ trợ tiếp cận. Compose cung cấp các API kiểm thử tích hợp để mô phỏng thao tác nhập bằng phím và xác minh trạng thái tiêu điểm.

Thiết lập kiểm thử trọng tâm

Để viết các bài kiểm thử tiêu điểm, hãy sử dụng createComposeRule() trong thư viện kiểm thử Compose:

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

Xác minh mục tiêu lấy tiêu điểm

Bạn có thể xác minh xem một phần tử trên giao diện người dùng có thể nhận tiêu điểm hay không bằng cách thực hiện một thao tác nhấp hoặc yêu cầu tiêu điểm, rồi xác nhận trạng thái của phần tử đó bằng cách sử dụng assertIsFocused() và 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()
}

Kiểm thử thao tác duyệt qua tiêu điểm theo một chiều

Mô phỏng các lần nhấn phím Tab và Shift+Tab bằng cách sử dụng performKeyInput để xác minh rằng tiêu điểm chuyển qua giao diện người dùng theo thứ tự xuất hiện. Theo phương pháp hay nhất, hãy xác minh cả thao tác điều hướng tiến (Key.Tab) và điều hướng ngược (Key.Tab khi giữ Key.ShiftLeft) để đảm bảo tính toàn vẹn của thao tác di chuyển hai chiều:

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

Kiểm thử quá trình duyệt qua theo hướng hai chiều

Mô phỏng các phím mũi tên hoặc phím định hướng trên D-pad bằng cách dùng Key.DirectionDown, Key.DirectionUp, Key.DirectionRight và 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()
}

Kiểm thử hành vi tiêu điểm của trường văn bản

Xác minh rằng các trường văn bản một dòng sẽ chuyển tiêu điểm khi nhấn phím Tab, trong khi các trường văn bản nhiều dòng sẽ giữ tiêu điểm:

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