1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package android.widget.espresso;
18
19import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
20
21import static com.android.internal.util.Preconditions.checkNotNull;
22
23import static org.hamcrest.Matchers.is;
24import static org.hamcrest.number.IsCloseTo.closeTo;
25
26import android.annotation.IntDef;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.support.test.espresso.NoMatchingViewException;
30import android.support.test.espresso.ViewAssertion;
31import android.text.Spanned;
32import android.text.TextUtils;
33import android.view.View;
34import android.widget.EditText;
35import android.widget.TextView;
36
37import junit.framework.AssertionFailedError;
38
39import org.hamcrest.Matcher;
40
41import java.lang.annotation.Retention;
42import java.lang.annotation.RetentionPolicy;
43
44/**
45 * A collection of assertions on a {@link android.widget.TextView}.
46 */
47public final class TextViewAssertions {
48
49    private TextViewAssertions() {}
50
51    /**
52     * Returns a {@link ViewAssertion} that asserts that the text view has a specified
53     * selection.<br>
54     * <br>
55     * View constraints:
56     * <ul>
57     * <li>must be a text view displayed on screen
58     * <ul>
59     *
60     * @param selection  The expected selection.
61     */
62    public static ViewAssertion hasSelection(String selection) {
63        return hasSelection(is(selection));
64    }
65
66    /**
67     * Returns a {@link ViewAssertion} that asserts that the text view has a specified
68     * selection.<br>
69     * <br>
70     * View constraints:
71     * <ul>
72     * <li>must be a text view displayed on screen
73     * <ul>
74     *
75     * @param selection  A matcher representing the expected selection.
76     */
77    public static ViewAssertion hasSelection(Matcher<String> selection) {
78        return new TextSelectionAssertion(selection);
79    }
80
81    /**
82     * Returns a {@link ViewAssertion} that asserts that the text view insertion pointer is at
83     * a specified index.<br>
84     * <br>
85     * View constraints:
86     * <ul>
87     * <li>must be a text view displayed on screen
88     * <ul>
89     *
90     * @param index  The expected index.
91     */
92    public static ViewAssertion hasInsertionPointerAtIndex(int index) {
93        return hasInsertionPointerAtIndex(is(index));
94    }
95
96    /**
97     * Returns a {@link ViewAssertion} that asserts that the text view insertion pointer is at
98     * a specified index.<br>
99     * <br>
100     * View constraints:
101     * <ul>
102     * <li>must be a text view displayed on screen
103     * <ul>
104     *
105     * @param index  A matcher representing the expected index.
106     */
107    public static ViewAssertion hasInsertionPointerAtIndex(final Matcher<Integer> index) {
108        return (view, exception) -> {
109            if (view instanceof TextView) {
110                TextView textView = (TextView) view;
111                int selectionStart = textView.getSelectionStart();
112                int selectionEnd = textView.getSelectionEnd();
113                try {
114                    assertThat(selectionStart, index);
115                    assertThat(selectionEnd, index);
116                } catch (IndexOutOfBoundsException e) {
117                    throw new AssertionFailedError(e.getMessage());
118                }
119            } else {
120                throw new AssertionFailedError("TextView not found");
121            }
122        };
123    }
124
125    /**
126     * Returns a {@link ViewAssertion} that asserts that the EditText insertion pointer is on
127     * the left edge.
128     */
129    public static ViewAssertion hasInsertionPointerOnLeft() {
130        return new CursorPositionAssertion(CursorPositionAssertion.LEFT);
131    }
132
133    /**
134     * Returns a {@link ViewAssertion} that asserts that the EditText insertion pointer is on
135     * the right edge.
136     */
137    public static ViewAssertion hasInsertionPointerOnRight() {
138        return new CursorPositionAssertion(CursorPositionAssertion.RIGHT);
139    }
140
141    /**
142     * Returns a {@link ViewAssertion} that asserts that the TextView does not contain styled text.
143     */
144    public static ViewAssertion doesNotHaveStyledText() {
145        return (view, exception) -> {
146            final CharSequence text = ((TextView) view).getText();
147            if (text instanceof Spanned && !TextUtils.hasStyleSpan((Spanned) text)) {
148                return;
149            }
150            throw new AssertionFailedError("TextView has styled text");
151        };
152    }
153
154    /**
155     * A {@link ViewAssertion} to check the selected text in a {@link TextView}.
156     */
157    private static final class TextSelectionAssertion implements ViewAssertion {
158
159        private final Matcher<String> mSelection;
160
161        public TextSelectionAssertion(Matcher<String> selection) {
162            mSelection = checkNotNull(selection);
163        }
164
165        @Override
166        public void check(View view, NoMatchingViewException exception) {
167            if (view instanceof TextView) {
168                TextView textView = (TextView) view;
169                int selectionStart = textView.getSelectionStart();
170                int selectionEnd = textView.getSelectionEnd();
171                try {
172                    String selectedText = textView.getText()
173                            .subSequence(selectionStart, selectionEnd)
174                            .toString();
175                    assertThat(selectedText, mSelection);
176                } catch (IndexOutOfBoundsException e) {
177                    throw new AssertionFailedError(e.getMessage());
178                }
179            } else {
180                throw new AssertionFailedError("TextView not found");
181            }
182        }
183    }
184
185    /**
186     * {@link ViewAssertion} to check that EditText cursor is on a given position.
187     */
188    static class CursorPositionAssertion implements ViewAssertion {
189
190        @Retention(RetentionPolicy.SOURCE)
191        @IntDef({LEFT, RIGHT})
192        public @interface CursorEdgePositionType {}
193        public static final int LEFT = 0;
194        public static final int RIGHT = 1;
195
196        private final int mPosition;
197
198        private CursorPositionAssertion(@CursorEdgePositionType int position) {
199            this.mPosition = position;
200        }
201
202        @Override
203        public void check(View view, NoMatchingViewException exception) {
204            if (!(view instanceof EditText)) {
205                throw new AssertionFailedError("View should be an instance of EditText");
206            }
207            EditText editText = (EditText) view;
208            Drawable drawable = editText.getEditorForTesting().getCursorDrawable();
209            Rect drawableBounds = drawable.getBounds();
210            Rect drawablePadding = new Rect();
211            drawable.getPadding(drawablePadding);
212
213            final int diff;
214            final String positionStr;
215            switch (mPosition) {
216                case LEFT:
217                    positionStr = "left";
218                    diff = drawableBounds.left - editText.getScrollX() + drawablePadding.left;
219                    break;
220                case RIGHT:
221                    positionStr = "right";
222                    int maxRight = editText.getWidth() - editText.getCompoundPaddingRight()
223                            - editText.getCompoundPaddingLeft() + editText.getScrollX();
224                    diff = drawableBounds.right - drawablePadding.right - maxRight;
225                    break;
226                default:
227                    throw new AssertionFailedError("Unknown position for cursor assertion");
228            }
229
230            assertThat("Cursor should be on the " + positionStr, Double.valueOf(diff),
231                    closeTo(0f, 1f));
232        }
233    }
234}
235