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