Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I test an annotated string hyperlink click in Jetpack Compose?

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!

like image 988
John A Qualls Avatar asked Feb 13 '26 14:02

John A Qualls


2 Answers

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))
    }
like image 195
Orcun Sevsay Avatar answered Feb 16 '26 02:02

Orcun Sevsay


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()
like image 24
Siva Velusamy Avatar answered Feb 16 '26 02:02

Siva Velusamy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!