Question

I am writing some tests for my Android app based on Espresso. After clicking on a link inside a TextView (created with the Linkify class) I need to assert I am seeing the correct screen.

I tried performing a click on the TextView that contains the link, but the link won't open.

Is there a proper way to test this using Espresso (other than modifying the code to have a separate TextView for the link)?

Was it helpful?

Solution

I found out a simpler way of doing this, there is a method in ViewActions called openLinkWithText, it uses linkTextMatcher as matcher to match link, using this is it very easy to click a textview with multiple links like this :

Espresso.onView(ViewMatchers.withId(R.id.legal_privacy_tv)).perform(ViewActions.openLinkWithText("Privacy Statement")); 

In my case i have 1 single text view which is linkify with 2 links, privacy statement and legal statement, using above method i am able to click on them individually without using bound xy or other methods suggested above.

OTHER TIPS

Here's my solution. I'm using reflection and it's ugly, but perhaps it serves someone else.

In my case the spans array contains several types of Spans. The ones I care about are some kind of inner anonymous class, thus getClass().getSimpleName() returns empty string. The instance holds a private field containing the URLSpan

private void clickOnSpan(TextView textView, CharSequence spanContent) {

SpannableString completeText = (SpannableString) textView.getText();
Layout textViewLayout = textView.getLayout();

Object[] spans = completeText.getSpans(0, completeText.length(), Object.class);
Object spanToLocate = null;
for (Object span : spans) {
  if (!span.getClass().getSimpleName().equals(""))
    continue;

  try {
    Field[] fields = span.getClass().getDeclaredFields();
    fields[1].setAccessible(true);
    URLSpan urlSpan = (URLSpan) fields[1].get(span);

    if (urlSpan.getURL().equals(spanContent)) {
      spanToLocate = span;
      break;
    }
  } catch (IllegalAccessException e) {
    e.printStackTrace();
  }
}

int endOffsetOfClickedText = completeText.getSpanEnd(spanToLocate);
float endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(endOffsetOfClickedText);

int clickedTextLine = textViewLayout.getLineForOffset(endOffsetOfClickedText);
int yEndCoordinateOfClickedText = textViewLayout.getLineBottom(clickedTextLine);

onView(withId(textView.getId())).perform(clickXY(Float.valueOf(endXCoordinatesOfClickedText).intValue(), yEndCoordinateOfClickedText / 2)); 
}

References:

clickXY: Click by bounds / coordinates

How get coordinate of a ClickableSpan inside a TextView?

Does it work when you click the link manually? Espresso sends the save events to the screen as a user tap does, so I'd be surprised if it didn't work (provided it was clicking on the same coordinates). That being said, if your link launches an Activity in an external application, the instrumentation test will fail due to security restrictions. There is currently no way to work around this with Espresso.

While the top voted answer is correct, maybe I can add little more icing to the pie. So, in my case, I also wanted to verify upon clicking the link the correct intent is being sent out and I recently found out there is a very handy sub-library in espresso for this exact scenario:

androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'

In @After method of test call Intents.init()

And in @Before method of test call Intents.release()

And in @Test method, do something like this -

@Test
fun testClickingUrlInTextView() {
  // preparing
  val link = "www.stackoverflow.com"
  val expectingIntent = Matchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasData(link));

  // mocking intent to prevent actual navigation during test
  Intents.intending(expectingIntent).respondWith(new ActivityResult(0, null));

  // performing action 
  onView(Matchers.allOf(withId(R.id.textview), withText(link)))
        .perform(openLinkWithText(link));

  // asserting our expected intent was recorded
  Intents.intended(expectingIntent);
}

So by structuring the test in the above manner, not only was I able to assert the navigating intent but also I was able to make the test less flaky in terms of it being able to run on any device regardless whether that device has the apps installed to handle that link. (Unless you want it to open in a specific app, which btw you can do as well by using the above espresso sub-lib.)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top