This answer demonstrates how to embed a link within an annotated string and make it clickable. This works great and triggers the on click with the correct URL. However, I can't seem to write a test that clicks the annotated text to open the link. Has anyone had success writing a test like this? My production code is very similar to what is in the answer. Below is my test code:
@Test
fun it_should_open_terms_of_service_link() {
val termsOfServiceText = getString(R.string.settings_terms)
try {
Intents.init()
stubAnyIntent()
composeTestRule.onNode(hasText(termsOfServiceText, substring = true)).performClick()
assertLinkWasOpened(getString(R.string.settings_terms_link))
} finally {
Intents.release()
}
}
It looks like hasText(termsOfServiceText, substring = true) fetches the entire annotated string node opposed to just the substring, "Terms of Service". Thus, the on click method does get triggered, just not at the correct position in the annotated string. Happy to provide more info if needed. Thanks!
I improved this answer. Unfortunately, hasText(termsOfServiceText, substring = true) is not fetching the substring part. Because of that, we need to perform touch input on the position of the text. If the text is not one line, it's super hard to find the correct offset point to click.
If that's the case, I managed to fix this issue. 2D array looping, tracing, and clicking each part of the text until we receive an Intent:
@Test
fun it_should_open_terms_of_service_link() {
Intents.init()
val expectedIntent = Matchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasData(termsOfServiceUri))
Intents.intending(expectedIntent).respondWith(Instrumentation.ActivityResult(0, null))
loop@ for (x in 0..100) {
for (y in 50..100) {
// Battleship style. Click second half of the Text from middle to bottom.
if (Intents.getIntents().size == 0)
clickOnTermsHyperlinkTextWithOffset(x / 100f, y / 100f)
else
break@loop
}
}
Intents.intended(expectedIntent)
Intents.release()
}
fun clickOnTermsHyperlinkTextWithOffset(x: Float, y: Float) =
composeRule?.onNodeWithTag(TERMS_TEST_TAG)?.performTouchInput {
click(percentOffset(x, y))
}
I ended up with something like the following based on code in the compose sources:
// Clicks on the first link in the given text node.
// This is a simplified version of `SemanticsNodeInteraction.performFirstLinkClick` present in compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
internal fun SemanticsNodeInteraction.clickFirstLink() {
val textLayoutResult = textLayoutResult()
val text = textLayoutResult.layoutInput.text
val linkAnnotations = text.getLinkAnnotations(0, text.length)
val boundsOfLinks = textLayoutResult.getBoundingBox(linkAnnotations.first().start)
performTouchInput { click(boundsOfLinks.center) }
}
// Returns the text layout result of the given text node.
private fun SemanticsNodeInteraction.textLayoutResult() : TextLayoutResult {
val textLayoutResults = mutableListOf<TextLayoutResult>()
performSemanticsAction(SemanticsActions.GetTextLayoutResult) {
it(textLayoutResults)
}
return textLayoutResults.first()
}
This can then be used as:
composeTestRule
.onNodeWithText("Learn more", substring = true)
.clickFirstLink()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With