18

In Jetpack compose I have a TextField and I'm trying to write Espresso UI tests.
I didn't find how I can enter text in the TextField, any ideas, please?

TextField(
    value = textState.value,
    modifier = Modifier.fillMaxWidth(),
    onValueChange = {
        textState.value = it
        apiServiceCall(textState.value.text)
    },
    keyboardOptions = KeyboardOptions(
        capitalization = KeyboardCapitalization.Sentences)
    ),
)

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun enterTextAndMakeServiceCall() {
    ActivityScenario.launch(MainActivity::class.java)

    // TODO: Enter text inside the TextField
    composeTestRule.onNode(hasText(getString(R.string.result)))
}
Abhimanyu
  • 11,351
  • 7
  • 51
  • 121
AndroidDev
  • 5,193
  • 5
  • 37
  • 68

3 Answers3

20

I first set the testTag modifier on the composable I want to test:

const val MY_TEXTFIELD_TAG = "myTextFieldTag"

TextField(
    value = textState.value,
    modifier = Modifier.fillMaxWidth().testTag(MY_TEXTFIELD_TAG),
    onValueChange = {
        textState.value = it
    },
    keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
)

And then from your test you can set and check the value like this:

@Test
fun setAndCheckTheTextFieldValue() {
    ActivityScenario.launch(MainActivity::class.java)
    val resultText = "result"

    // Sets the TextField value
    composeTestRule.onNodeWithTag(MY_TEXTFIELD_TAG).performTextInput(resultText)

    // Asserts the TextField has the corresponding value
    composeTestRule.onNodeWithTag(MY_TEXTFIELD_TAG).assert(hasText(resultText))
}

UPDATE:

Another way I use lately is to use the contentDescription instead.

Let's say you have a TextField with content description like this one (not using state hoisting for simplicity on this sample):

@Composable
fun MyTextField() {
    val textState = remember { mutableStateOf(TextFieldValue()) }
    val textFieldContentDescription = stringResource(id = R.string.text_field_content_description)
    TextField(
        value = textState.value,
        modifier = Modifier
            .fillMaxWidth()
            .semantics { contentDescription = textFieldContentDescription },
        onValueChange = {
            textState.value = it
        },
    )
}

The test could be something like:

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun setAndCheckTheTextFieldValue() {
    lateinit var textFieldContentDescription: String
    composeTestRule.setContent {
        textFieldContentDescription = stringResource(id = R.string.text_field_content_description)
        MaterialTheme {
            MyTextField()
        }
    }
    val resultText = "result"

    // Sets the TextField value
    composeTestRule.onNodeWithContentDescription(textFieldContentDescription).performTextInput(resultText)

    // Asserts the TextField has the corresponding value
    composeTestRule.onNodeWithContentDescription(textFieldContentDescription).assert(hasText(resultText, ignoreCase = true))
}

and this way the app is more accessible as well by having content descriptions.

jeprubio
  • 17,312
  • 5
  • 45
  • 56
  • I don't think that adding Testing Code into the production codebase is a good idea. Instead I would test using: composeTestRule.onNodeWithText(text = "result"). – Fernando Prieto Moyano Dec 06 '21 at 14:54
  • but if its an input, you don't have any test "result" until you add it, which you need to know the element for in the first place. – Brill Pappin Feb 23 '22 at 19:57
  • I've updated the answer adding a sample of using `contentDescription` instead. This way you don't have to add this tag in production codebase and you don't rely on any previous set text while improving the accessibility of the code. – jeprubio Feb 24 '22 at 09:27
  • I'm getting an error: `Failed to perform text input. Failed to assert the following: (SetText is defined)` while executing `.performTextInput(text)`. Any idea why? – kosiara - Bartosz Kosarzycki Jul 20 '23 at 11:38
  • 1
    @kosiara-BartoszKosarzycki are you sure you are matching a TextField and not an element that wraps it or something like that? Cause that error seems to be about using .performTextInput() on something that doesn't seem to be a TextField. – jeprubio Aug 11 '23 at 11:06
1

If you have set a label to your empty TextField then you can set text without the need for any test tags or content description like below:

TextField(
  ...
  label = "Production Text",
  ...
)

Test Code:

composeTestRule
  .onNodeWithText("Production Text", useUnmergedTree = true) 
  .onParent()
  .performTextInput("Test Text")
-1

Yo just can find by nodeText you need to search the string in label property

composeTestRule.onNodeWithText("user").performTextInput("userCred")

That will find that Textfield and will enter the text "userCred".

This solution only works if you don't have any other text that matches that string.