https://kotlinlang.org logo
#compose
Title
# compose
n

nitrog42

04/29/2022, 3:39 PM
Today, I tried to test some code, and of course I regret it now 🙃. I'm struggling to understand what's the best way to remove
delay()
in a LaunchedEffect code in reply
Copy code
@Test
fun test() {
    val testDispatcher = StandardTestDispatcher()
    composeTestRule.setContent {
        Box {
            var text by remember {
                mutableStateOf("")
            }
            LaunchedEffect(Unit) {
                var index = 0
                while (isActive) {
                    withContext(testDispatcher) {
                        delay(1000)
                    }
                    text = "Hello $index"
                }
            }
            Text(text = text)
        }
    }
    composeTestRule.waitUntil {
        composeTestRule.onAllNodesWithText("Hello 1").fetchSemanticsNodes().size == 1
    }
    composeTestRule.waitUntil {
        composeTestRule.onAllNodesWithText("Hello 2").fetchSemanticsNodes().size == 1
    }
}
I tried runTests, and I know that
advanceUntilIdle
works, but the issue I have is that in my screen the LaunchedEffect/delay can run multiple time, and my test will not be aware of that what I'm struggling to understand is that :
Copy code
LaunchedEffect(Unit) {
    withContext(testDispatcher) {
        delay(1)
    }
}
will lock the test unless I call
testDispatcher.scheduler.advanceTime()
when I just want to "always" skip the delays
If you want the "real" compose use case i'm writing something in a testfield :
Copy code
composeTestRule.onNodeWithText(phoneNumberHintField).performTextInput(validPhoneNumber)
which should trigger a LaunchedEffect :
Copy code
LaunchedEffect(text) {
    withContext(debounceDispatcher) {
        delay(600) //Debounce
        //heavy operation on text
        text = newText
    }
}
and
Copy code
composeTestRule.onNodeWithText(phoneNumberHintField).performTextInput(validPhoneNumber)
debounceDispatcher.scheduler.advanceUntilIdle()
Is basically not working, probably running too soon ? not sure why but it seems there is a delay on the performtextinput and Launchedeffect run...
I slept over this for the week-end and now the solution seems simple :
Copy code
composeTestRule.waitUntil {
        advanceUntilIdle()
        composeTestRule.onAllNodesWithText("Hello").fetchSemanticsNodes().size == 1
    }
z

Zach Klippenstein (he/him) [MOD]

05/17/2022, 11:14 PM
Do you need the
advanceUntilIdle()
in that block? I thought
waitUntil
did that implicitly.
n

nitrog42

05/18/2022, 8:07 AM
hmm I call
advanceUntilIdle()
because I run the composeTest with
runTest { }
like this :
Copy code
@Test
    fun testFormatValidNumber() = runTest {
        //The testdispatcher is mandatory here as we want to avoid the delay of a debounce for the test
        val testDispatcher = StandardTestDispatcher(this.testScheduler)
        composeTestRule.setContent {
            
                EnterPhoneScreen(onNavigateUp = { }, onValidateClicked = {}, parseDispatcher = testDispatcher)
            
        }
        val enteredPhoneNumber = "0612345678"
        val displayedPhoneNumber = "06 12 34 56 78"
        val formattedPhoneNumber = "+33 6 12 34 56 78"

        composeTestRule.onNodeWithText(phoneNumberHintField).performTextInput(enteredPhoneNumber)

        composeTestRule.waitUntil {
            composeTestRule.onAllNodesWithText(displayedPhoneNumber).fetchSemanticsNodes().size == 1
        }
        composeTestRule.waitUntil {
            advanceUntilIdle()
            composeTestRule.onAllNodesWithText(formattedPhoneNumber).fetchSemanticsNodes().size == 1
        }
        composeTestRule.onNodeWithText(validateButtonText).assertIsDisplayed().assertIsEnabled()
    }
unfortunatelly.. it sometimes fails on our CI, maybe because of high CPU load (shared by multiple projects) so I disabled it for now if you see something in this code that seems weird, feel free to tell me 😄
I use a specific dispatcher because the screen contains a phone number field which I reformat using libphonenumber :
Copy code
LaunchedEffect(phoneNumberText.value) {
            Timber.d("parsing number $phoneNumberText")
            phoneNumber = null
            // just a simple debounce
            delay(600L)
            withContext(parseDispatcher) {
                try {
                    // We parse the number entered by the user only if it's detected as a mobile phone number
                    phoneNumber = phoneNumberUtil.parseNumberIfMobile(phoneNumberText.value.text)?.also {
                        //We reformat the number as E164
                        val newFormattedNumber = phoneNumberUtil.format(it, PhoneNumberUtil.PhoneNumberFormat.E164)
                        //We update the visual with the new formatted number (E164) ; fromUser = false
                        phoneNumberText = TextFieldWithChangeSource(phoneNumberText.value.updatePhone(newFormattedNumber))
                    }
                } catch (e: Exception) {
                    Timber.e(e)
                }
                Timber.d("parsed number $phoneNumberText")
            }
        }
(phoneNumber is the PhoneNumber class, phoneNumberText is the standard String that I use in the field ; TextFieldWithChangeSource is a specific class I made to know if the user wrote the change or it comes from the “parser/reformatter” that takes a few hundreds of millisec)
28 Views