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