1/*
2 * Copyright (C) 2017 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 */
16package androidx.emoji.util;
17
18import static org.mockito.Matchers.argThat;
19
20import android.text.Spanned;
21import android.text.TextUtils;
22
23import androidx.emoji.text.EmojiSpan;
24
25import org.hamcrest.Description;
26import org.hamcrest.Matcher;
27import org.hamcrest.TypeSafeMatcher;
28import org.mockito.ArgumentMatcher;
29
30/**
31 * Utility class that includes matchers specific to emojis and EmojiSpans.
32 */
33public class EmojiMatcher {
34
35    public static Matcher<CharSequence> hasEmojiAt(final int id, final int start,
36            final int end) {
37        return new EmojiResourceMatcher(id, start, end);
38    }
39
40    public static Matcher<CharSequence> hasEmojiAt(final Emoji.EmojiMapping emojiMapping,
41            final int start, final int end) {
42        return new EmojiResourceMatcher(emojiMapping.id(), start, end);
43    }
44
45    public static Matcher<CharSequence> hasEmojiAt(final int start, final int end) {
46        return new EmojiResourceMatcher(-1, start, end);
47    }
48
49    public static Matcher<CharSequence> hasEmoji(final int id) {
50        return new EmojiResourceMatcher(id, -1, -1);
51    }
52
53    public static Matcher<CharSequence> hasEmoji(final Emoji.EmojiMapping emojiMapping) {
54        return new EmojiResourceMatcher(emojiMapping.id(), -1, -1);
55    }
56
57    public static Matcher<CharSequence> hasEmoji() {
58        return new EmojiSpanMatcher();
59    }
60
61    public static Matcher<CharSequence> hasEmojiCount(final int count) {
62        return new EmojiCountMatcher(count);
63    }
64
65    public static <T extends CharSequence> T sameCharSequence(final T expected) {
66        return argThat(new ArgumentMatcher<T>() {
67            @Override
68            public boolean matches(T o) {
69                if (o instanceof CharSequence) {
70                    return TextUtils.equals(expected, o);
71                }
72                return false;
73            }
74
75            @Override
76            public String toString() {
77                return "doesn't match " + expected;
78            }
79        });
80    }
81
82    private static class EmojiSpanMatcher extends TypeSafeMatcher<CharSequence> {
83
84        private EmojiSpan[] mSpans;
85
86        EmojiSpanMatcher() {
87        }
88
89        @Override
90        public void describeTo(Description description) {
91            description.appendText("should have EmojiSpans");
92        }
93
94        @Override
95        protected void describeMismatchSafely(final CharSequence charSequence,
96                Description mismatchDescription) {
97            mismatchDescription.appendText(" has no EmojiSpans");
98        }
99
100        @Override
101        protected boolean matchesSafely(final CharSequence charSequence) {
102            if (charSequence == null) return false;
103            if (!(charSequence instanceof Spanned)) return false;
104            mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
105            return mSpans.length != 0;
106        }
107    }
108
109    private static class EmojiCountMatcher extends TypeSafeMatcher<CharSequence> {
110
111        private final int mCount;
112        private EmojiSpan[] mSpans;
113
114        EmojiCountMatcher(final int count) {
115            mCount = count;
116        }
117
118        @Override
119        public void describeTo(Description description) {
120            description.appendText("should have ").appendValue(mCount).appendText(" EmojiSpans");
121        }
122
123        @Override
124        protected void describeMismatchSafely(final CharSequence charSequence,
125                Description mismatchDescription) {
126            mismatchDescription.appendText(" has ");
127            if (mSpans == null) {
128                mismatchDescription.appendValue("no");
129            } else {
130                mismatchDescription.appendValue(mSpans.length);
131            }
132
133            mismatchDescription.appendText(" EmojiSpans");
134        }
135
136        @Override
137        protected boolean matchesSafely(final CharSequence charSequence) {
138            if (charSequence == null) return false;
139            if (!(charSequence instanceof Spanned)) return false;
140            mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
141            return mSpans.length == mCount;
142        }
143    }
144
145    private static class EmojiResourceMatcher extends TypeSafeMatcher<CharSequence> {
146        private static final int ERR_NONE = 0;
147        private static final int ERR_SPANNABLE_NULL = 1;
148        private static final int ERR_NO_SPANS = 2;
149        private static final int ERR_WRONG_INDEX = 3;
150        private final int mResId;
151        private final int mStart;
152        private final int mEnd;
153        private int mError = ERR_NONE;
154        private int mActualStart = -1;
155        private int mActualEnd = -1;
156
157        EmojiResourceMatcher(int resId, int start, int end) {
158            mResId = resId;
159            mStart = start;
160            mEnd = end;
161        }
162
163        @Override
164        public void describeTo(final Description description) {
165            if (mResId == -1) {
166                description.appendText("should have EmojiSpan at ")
167                        .appendValue("[" + mStart + "," + mEnd + "]");
168            } else if (mStart == -1 && mEnd == -1) {
169                description.appendText("should have EmojiSpan with resource id ")
170                        .appendValue(Integer.toHexString(mResId));
171            } else {
172                description.appendText("should have EmojiSpan with resource id ")
173                        .appendValue(Integer.toHexString(mResId))
174                        .appendText(" at ")
175                        .appendValue("[" + mStart + "," + mEnd + "]");
176            }
177        }
178
179        @Override
180        protected void describeMismatchSafely(final CharSequence charSequence,
181                Description mismatchDescription) {
182            int offset = 0;
183            mismatchDescription.appendText("[");
184            while (offset < charSequence.length()) {
185                int codepoint = Character.codePointAt(charSequence, offset);
186                mismatchDescription.appendText(Integer.toHexString(codepoint));
187                offset += Character.charCount(codepoint);
188                if (offset < charSequence.length()) {
189                    mismatchDescription.appendText(",");
190                }
191            }
192            mismatchDescription.appendText("]");
193
194            switch (mError) {
195                case ERR_NO_SPANS:
196                    mismatchDescription.appendText(" had no spans");
197                    break;
198                case ERR_SPANNABLE_NULL:
199                    mismatchDescription.appendText(" was null");
200                    break;
201                case ERR_WRONG_INDEX:
202                    mismatchDescription.appendText(" had Emoji at ")
203                            .appendValue("[" + mActualStart + "," + mActualEnd + "]");
204                    break;
205                default:
206                    mismatchDescription.appendText(" does not have an EmojiSpan with given "
207                            + "resource id ");
208            }
209        }
210
211        @Override
212        protected boolean matchesSafely(final CharSequence charSequence) {
213            if (charSequence == null) {
214                mError = ERR_SPANNABLE_NULL;
215                return false;
216            }
217
218            if (!(charSequence instanceof Spanned)) {
219                mError = ERR_NO_SPANS;
220                return false;
221            }
222
223            Spanned spanned = (Spanned) charSequence;
224            final EmojiSpan[] spans = spanned.getSpans(0, charSequence.length(), EmojiSpan.class);
225
226            if (spans.length == 0) {
227                mError = ERR_NO_SPANS;
228                return false;
229            }
230
231            if (mStart == -1 && mEnd == -1) {
232                for (int index = 0; index < spans.length; index++) {
233                    if (mResId == spans[index].getId()) {
234                        return true;
235                    }
236                }
237                return false;
238            } else {
239                for (int index = 0; index < spans.length; index++) {
240                    if (mResId == -1 || mResId == spans[index].getId()) {
241                        mActualStart = spanned.getSpanStart(spans[index]);
242                        mActualEnd = spanned.getSpanEnd(spans[index]);
243                        if (mActualStart == mStart && mActualEnd == mEnd) {
244                            return true;
245                        }
246                    }
247                }
248
249                if (mActualStart != -1 && mActualEnd != -1) {
250                    mError = ERR_WRONG_INDEX;
251                }
252
253                return false;
254            }
255        }
256    }
257}
258