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

Việc tự động hoá các bài kiểm thử điều hướng bằng tiêu điểm giúp đả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ông cụ hỗ trợ tiếp cận. Compose cung cấp các API kiểm thử tích hợp sẵn để mô phỏng hoạt động nhập bằng phím và xác minh trạng thái tiêu điểm.

Thiết lập thử nghiệm tập trung

Để viết mã 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 nhấp hoặc yêu cầu tiêu điểm, sau đó xác nhận trạng thái của phần tử đó bằ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ử tính năng duyệt qua tiêu điểm theo một chiều

Mô phỏng thao tác nhấn phím Tab và Shift+Tab bằng performKeyInput để xác minh rằng tiêu điểm di chuyển qua giao diện người dùng theo thứ tự xuất hiện. Tốt nhất là bạn nên xác minh cả thao tác di chuyển tiến (Key.Tab) và di chuyển lùi (Key.Tab khi nhấn 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ử thao tác duyệt qua theo hướng hai chiều

Mô phỏng các phím mũi tên hoặc phím chỉ hướng D-pad bằng cách sử 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 lấy 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 vẫn 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()
}