How to click a clickablespan using espresso?
This is my solution. It's simpler because we don't need to find the coordinates. Once we have found the ClickableSpan, we just click on it:
public static ViewAction clickClickableSpan(final CharSequence textToClick) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return Matchers.instanceOf(TextView.class);
}
@Override
public String getDescription() {
return "clicking on a ClickableSpan";
}
@Override
public void perform(UiController uiController, View view) {
TextView textView = (TextView) view;
SpannableString spannableString = (SpannableString) textView.getText();
if (spannableString.length() == 0) {
// TextView is empty, nothing to do
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
if (spans.length > 0) {
ClickableSpan spanCandidate;
for (ClickableSpan span : spans) {
spanCandidate = span;
int start = spannableString.getSpanStart(spanCandidate);
int end = spannableString.getSpanEnd(spanCandidate);
CharSequence sequence = spannableString.subSequence(start, end);
if (textToClick.toString().equals(sequence.toString())) {
span.onClick(textView);
return;
}
}
}
// textToClick not found in TextView
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
};
}
Now you can use our custom ViewAction just like that:
onView(withId(R.id.myTextView)).perform(clickClickableSpan("myLink"));
The best option would be to subclass a ViewAction. Here is the way of doing it in Kotlin:
class SpannableTextClickAction(val text: String) : ViewAction {
override fun getDescription(): String = "SpannableText click action"
override fun getConstraints(): Matcher<View> =
isAssignableFrom(TextView::class.java)
override fun perform(uiController: UiController?, view: View?) {
val textView = view as TextView
val spannableString = textView.text as SpannableString
val spans = spannableString.getSpans(0, spannableString.count(), ClickableSpan::class.java)
val spanToLocate = spans.firstOrNull { span: ClickableSpan ->
val start = spannableString.getSpanStart(span)
val end = spannableString.getSpanEnd(span)
val spanText = spannableString.subSequence(start, end).toString()
spanText == text
}
if (spanToLocate != null) {
spanToLocate.onClick(textView)
return
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
and use it as:
onView(withId(<view_id>)).perform(scrollTo(), SpannableTextClickAction(text))
Here is the Kotlin version of accepted answer
fun clickClickableSpan(textToClick: CharSequence): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return Matchers.instanceOf(TextView::class.java)
}
override fun getDescription(): String {
return "clicking on a ClickableSpan";
}
override fun perform(uiController: UiController, view: View) {
val textView = view as TextView
val spannableString = textView.text as SpannableString
if (spannableString.isEmpty()) {
// TextView is empty, nothing to do
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java)
if (spans.isNotEmpty()) {
var spanCandidate: ClickableSpan
for (span: ClickableSpan in spans) {
spanCandidate = span
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString().equals(sequence.toString())) {
span.onClick(textView)
return;
}
}
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
}