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