如何使用浓缩咖啡点击clickablespan?

时间:2016-07-11 18:49:12

标签: android testing android-espresso

我有一个带有多个可点击跨度的文本视图。我希望能够测试点击这些跨度。

我尝试设置一个自定义ViewAction,它可以在TextView中找到clickablespans,然后将它们的文本与所需的文本相匹配,然后单击该文本的xy坐标。但是,似乎添加到TextView的跨度不是ClickableSpan类型,而是添加了跨度的片段。

因此,我无法区分链接范围。有更好的方法吗?

添加范围:

Util.addClickableSpan(spannableString, string, linkedString, new      ClickableSpan() {
@Override
public void onClick(View textView) {}
});

tvAcceptTc.setText(spannableString);
tvAcceptTc.setMovementMethod(LinkMovementMethod.getInstance());

效用方法:

public static void addClickableSpan(SpannableString spannableString,
                              String text,
                              String subText,
                              ClickableSpan clickableSpan) {
        int start = text.indexOf(subText);
        int end = text.indexOf(subText) + subText.length();
        int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;

        spannableString.setSpan(clickableSpan, start, end, flags);
}

定义ViewAction:

@Override
        public void perform(UiController uiController, View view) {
            uiController.loopMainThreadUntilIdle();
            if (view instanceof TextView) {

                TextView textView = (TextView) view;
                Layout textViewLayout = textView.getLayout();


                SpannableString fullSpannable = new SpannableString(textView.getText());

                Object[] spans = fullSpannable.getSpans(0, fullSpannable.length(), Object.class);

                ClickableSpan span = null;
                for (Object object : spans) {
                    if (object instanceof BaseFragment) {
                        ClickableSpan foundSpan = (ClickableSpan)object;
                        int spanStart = fullSpannable.getSpanStart(foundSpan);
                        int spanEnd = fullSpannable.getSpanEnd(foundSpan);
                        if (fullSpannable.subSequence(spanStart, spanEnd).equals(aSubstring)) {
                            //Found the correct span!
                            span = foundSpan;
                        }
                    }
                } ... go on to click the xy-coordinates

7 个答案:

答案 0 :(得分:24)

这是我的解决方案。它更简单,因为我们不需要找到坐标。找到ClickableSpan后,我们只需点击它:

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();

        }
    };
}

现在您可以使用我们的自定义ViewAction:

    onView(withId(R.id.myTextView)).perform(clickClickableSpan("myLink"));

答案 1 :(得分:0)

这对我有用:

/**
 * Clicks the first ClickableSpan in the TextView
 */
public static ViewAction clickFirstClickableSpan() {
    return new GeneralClickAction(
            Tap.SINGLE,
            new CoordinatesProvider() {
                @Override
                public float[] calculateCoordinates(View view) {
                    //https://leons.im/posts/how-to-get-coordinate-of-a-clickablespan-inside-a-textview/
                    TextView textView = (TextView) view;
                    Rect parentTextViewRect = new Rect();

                    SpannableString spannableString = (SpannableString) textView.getText();
                    Layout textViewLayout = textView.getLayout();
                    ClickableSpan spanToLocate = null;

                    if (spannableString.length() == 0) {
                        return new float[2];
                    }

                    ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
                    if (spans.length > 0) {
                        spanToLocate = spans[0];
                    }

                    if (spanToLocate == null) {
                        // no specific view found
                        throw new NoMatchingViewException.Builder()
                                .includeViewHierarchy(true)
                                .withRootView(textView)
                                .build();
                    }

                    double startOffsetOfClickedText = spannableString.getSpanStart(spanToLocate);
                    double endOffsetOfClickedText = spannableString.getSpanEnd(spanToLocate);
                    double startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int) startOffsetOfClickedText);
                    double endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int) endOffsetOfClickedText);

                    // Get the rectangle of the clicked text
                    int currentLineStartOffset = textViewLayout.getLineForOffset((int) startOffsetOfClickedText);
                    int currentLineEndOffset = textViewLayout.getLineForOffset((int) endOffsetOfClickedText);
                    boolean keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset;
                    textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect);

                    // Update the rectangle position to his real position on screen
                    int[] parentTextViewLocation = {0, 0};
                    textView.getLocationOnScreen(parentTextViewLocation);

                    double parentTextViewTopAndBottomOffset = (
                            parentTextViewLocation[1] -
                                    textView.getScrollY() +
                                    textView.getCompoundPaddingTop()
                    );
                    parentTextViewRect.top += parentTextViewTopAndBottomOffset;
                    parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
                    parentTextViewRect.left += (
                            parentTextViewLocation[0] +
                                    startXCoordinatesOfClickedText +
                                    textView.getCompoundPaddingLeft() -
                                    textView.getScrollX()
                    );
                    parentTextViewRect.right = (int) (
                            parentTextViewRect.left +
                                    endXCoordinatesOfClickedText -
                                    startXCoordinatesOfClickedText
                    );

                    int screenX = (parentTextViewRect.left + parentTextViewRect.right) / 2;
                    int screenY = (parentTextViewRect.top + parentTextViewRect.bottom) / 2;
                    if (keywordIsInMultiLine) {
                        screenX = parentTextViewRect.left;
                        screenY = parentTextViewRect.top;
                    }
                    return new float[]{screenX, screenY};
                }
            },
            Press.FINGER);
}

答案 2 :(得分:0)

您可以使用Spannable代替与SpannableString兼容的SpannableStringBuilder

抱歉,我是一个新人,只有1个声望,无法添加评论。即使我的英语很差......

我建议使用:

Spannable spannableString = (Spannable) textView.getText();

而不是:

SpannableString spannableString = (SpannableString) textView.getText();

发布以下所有代码:

public class CustomViewActions {

    /**
     * click specific spannableString
     */
    public static ViewAction clickClickableSpan(final CharSequence textToClick) {
        return clickClickableSpan(-1, textToClick);
    }

    /**
     * click the first spannableString
     */
    public static ViewAction clickClickableSpan() {
        return clickClickableSpan(0, null);
    }

    /**
     * click the nth spannableString
     */
    public static ViewAction clickClickableSpan(final int index) {
        return clickClickableSpan(index, null);
    }

    public static ViewAction clickClickableSpan(final int index,final CharSequence textToClick) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return instanceOf(TextView.class);
            }

            @Override
            public String getDescription() {
                return "clicking on a ClickableSpan";
            }

            @Override
            public void perform(UiController uiController, View view) {
                TextView textView = (TextView) view;
                Spannable spannableString = (Spannable) textView.getText();
                ClickableSpan spanToLocate = null;
                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) {
                    if(index >=spans.length){
                        throw new NoMatchingViewException.Builder()
                            .includeViewHierarchy(true)
                            .withRootView(textView)
                            .build();
                    }else if (index >= 0) {
                        spanToLocate = spans[index];
                        spanToLocate.onClick(textView);
                        return;
                    }
                    for (int i = 0; i < spans.length; i++) {
                        int start = spannableString.getSpanStart(spans[i]);
                        int end = spannableString.getSpanEnd(spans[i]);
                        CharSequence sequence = spannableString.subSequence(start, end);
                        if (textToClick.toString().equals(sequence.toString())) {
                            spanToLocate = spans[i];
                            spanToLocate.onClick(textView);
                            return;
                        }
                    }
                }

                // textToClick not found in TextView
                throw new NoMatchingViewException.Builder()
                        .includeViewHierarchy(true)
                        .withRootView(textView)
                        .build();

            }
        };
    }

}

答案 3 :(得分:0)

最好的选择是将ViewAction子类化。这是在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()
    }
}

并将其用作:

onView(withId(<view_id>)).perform(scrollTo(), SpannableTextClickAction(text))

答案 4 :(得分:0)

这是接受答案的Kotlin版本

aws ec2 describe-instance-status --instance-id i-xxxxxxxxxxx
{
    "InstanceStatuses": [
        {
            "AvailabilityZone": "",
            "InstanceId": "i-xxxxxxxxxxx",
            "InstanceState": {
                "Code": 16,
                "Name": "running"
            },
            "InstanceStatus": {
                "Details": [
                    {
                        "Name": "reachability",
                        "Status": "initializing"
                    }
                ],
                "Status": "initializing"
            },
            "SystemStatus": {
                "Details": [
                    {
                        "Name": "reachability",
                        "Status": "initializing"
                    }
                ],
                "Status": "initializing"
            }
        }
    ]
}

答案 5 :(得分:0)

它做了一点改动。
只需在以下位置重新检查“ textToClick”和变量“ sequence”即可:

CharSequence sequence = spannableString.subSequence(start, end);

完全一样。

我必须像这样使用trim():

textToClick.toString() == sequence.trim().toString()

因为我的textToClick值是“ click here”,而序列值是“ click here”

注意:“点击”之前的空格。

我希望这对某人有用。

答案 6 :(得分:0)

Espresso为此提供了一个内衬:

onView(withId(R.id.textView)).perform(openLinkWithText("..."))