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 */ 16 package androidx.emoji.util; 17 18 import static org.mockito.Matchers.argThat; 19 20 import android.text.Spanned; 21 import android.text.TextUtils; 22 23 import androidx.emoji.text.EmojiSpan; 24 25 import org.hamcrest.Description; 26 import org.hamcrest.Matcher; 27 import org.hamcrest.TypeSafeMatcher; 28 import org.mockito.ArgumentMatcher; 29 30 /** 31 * Utility class that includes matchers specific to emojis and EmojiSpans. 32 */ 33 public 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