Home | History | Annotate | Download | only in util
      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