1 /*
2  * Copyright (C) 2015 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 android.widget;
18 
19 import static android.widget.espresso.DragHandleUtils.onHandleView;
20 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarContainsItem;
21 import static android.widget.espresso.FloatingToolbarEspressoUtils.clickFloatingToolbarItem;
22 import static android.widget.espresso.FloatingToolbarEspressoUtils.sleepForFloatingToolbarPopup;
23 import static android.widget.espresso.SuggestionsPopupwindowUtils.assertSuggestionsPopupContainsItem;
24 import static android.widget.espresso.SuggestionsPopupwindowUtils.assertSuggestionsPopupIsDisplayed;
25 import static android.widget.espresso.SuggestionsPopupwindowUtils.assertSuggestionsPopupIsNotDisplayed;
26 import static android.widget.espresso.SuggestionsPopupwindowUtils.clickSuggestionsPopupItem;
27 import static android.widget.espresso.SuggestionsPopupwindowUtils.onSuggestionsPopup;
28 import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
29 import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
30 
31 import static androidx.test.espresso.Espresso.onView;
32 import static androidx.test.espresso.Espresso.pressBack;
33 import static androidx.test.espresso.action.ViewActions.clearText;
34 import static androidx.test.espresso.action.ViewActions.click;
35 import static androidx.test.espresso.action.ViewActions.replaceText;
36 import static androidx.test.espresso.assertion.ViewAssertions.matches;
37 import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
38 import static androidx.test.espresso.matcher.ViewMatchers.withId;
39 import static androidx.test.espresso.matcher.ViewMatchers.withText;
40 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
41 
42 import static org.hamcrest.Matchers.is;
43 import static org.junit.Assert.assertEquals;
44 import static org.junit.Assert.assertFalse;
45 import static org.junit.Assert.assertNotNull;
46 import static org.junit.Assert.assertTrue;
47 
48 import android.content.res.TypedArray;
49 import android.text.Selection;
50 import android.text.Spannable;
51 import android.text.Spanned;
52 import android.text.TextPaint;
53 import android.text.style.SuggestionSpan;
54 import android.text.style.TextAppearanceSpan;
55 
56 import androidx.test.filters.SmallTest;
57 import androidx.test.rule.ActivityTestRule;
58 
59 import com.android.frameworks.coretests.R;
60 
61 import org.junit.Rule;
62 import org.junit.Test;
63 
64 /**
65  * SuggestionsPopupWindowTest tests.
66  *
67  * TODO: Add tests for when there are no suggestions
68  */
69 @SmallTest
70 public class SuggestionsPopupWindowTest {
71 
72     @Rule
73     public final ActivityTestRule<TextViewActivity> mActivityRule =
74             new ActivityTestRule<>(TextViewActivity.class);
75 
getActivity()76     private TextViewActivity getActivity() {
77         return mActivityRule.getActivity();
78     }
79 
setSuggestionSpan(SuggestionSpan span, int start, int end)80     private void setSuggestionSpan(SuggestionSpan span, int start, int end) {
81         final TextView textView = getActivity().findViewById(R.id.textview);
82         textView.post(
83                 () -> {
84                     final Spannable text = (Spannable) textView.getText();
85                     text.setSpan(span, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
86                     Selection.setSelection(text, (start + end) / 2);
87                 });
88         getInstrumentation().waitForIdleSync();
89     }
90 
91     @Test
testOnTextContextMenuItem()92     public void testOnTextContextMenuItem() {
93         final String text = "abc def ghi";
94 
95         onView(withId(R.id.textview)).perform(click());
96         onView(withId(R.id.textview)).perform(replaceText(text));
97 
98         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
99                 new String[]{"DEF", "Def"}, SuggestionSpan.FLAG_AUTO_CORRECTION);
100         setSuggestionSpan(suggestionSpan, text.indexOf('d'), text.indexOf('f') + 1);
101 
102         final TextView textView = getActivity().findViewById(R.id.textview);
103         textView.post(() -> textView.onTextContextMenuItem(TextView.ID_REPLACE));
104         getInstrumentation().waitForIdleSync();
105 
106         assertSuggestionsPopupIsDisplayed();
107     }
108 
109     @Test
testSelectionActionMode()110     public void testSelectionActionMode() {
111         final String text = "abc def ghi";
112 
113         onView(withId(R.id.textview)).perform(click());
114         onView(withId(R.id.textview)).perform(replaceText(text));
115 
116         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
117                 new String[]{"DEF", "Def"}, SuggestionSpan.FLAG_AUTO_CORRECTION);
118         setSuggestionSpan(suggestionSpan, text.indexOf('d'), text.indexOf('f') + 1);
119 
120         onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('e')));
121         sleepForFloatingToolbarPopup();
122         assertFloatingToolbarContainsItem(
123                 getActivity().getString(com.android.internal.R.string.replace));
124         sleepForFloatingToolbarPopup();
125         clickFloatingToolbarItem(
126                 getActivity().getString(com.android.internal.R.string.replace));
127 
128         assertSuggestionsPopupIsDisplayed();
129     }
130 
131     @Test
testInsertionActionMode()132     public void testInsertionActionMode() {
133         final String text = "abc def ghi";
134 
135         onView(withId(R.id.textview)).perform(click());
136         onView(withId(R.id.textview)).perform(replaceText(text));
137 
138         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
139                 new String[]{"DEF", "Def"}, SuggestionSpan.FLAG_AUTO_CORRECTION);
140         setSuggestionSpan(suggestionSpan, text.indexOf('d'), text.indexOf('f') + 1);
141 
142         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.indexOf('e')));
143         onHandleView(com.android.internal.R.id.insertion_handle).perform(click());
144         sleepForFloatingToolbarPopup();
145         assertFloatingToolbarContainsItem(
146                 getActivity().getString(com.android.internal.R.string.replace));
147         clickFloatingToolbarItem(
148                 getActivity().getString(com.android.internal.R.string.replace));
149 
150         assertSuggestionsPopupIsDisplayed();
151     }
152 
showSuggestionsPopup()153     private void showSuggestionsPopup() {
154         final TextView textView = getActivity().findViewById(R.id.textview);
155         textView.post(() -> textView.onTextContextMenuItem(TextView.ID_REPLACE));
156         getInstrumentation().waitForIdleSync();
157         assertSuggestionsPopupIsDisplayed();
158     }
159 
160     @Test
testSuggestionItems()161     public void testSuggestionItems() {
162         final String text = "abc def ghi";
163 
164         onView(withId(R.id.textview)).perform(click());
165         onView(withId(R.id.textview)).perform(replaceText(text));
166 
167         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
168                 new String[]{"DEF", "Def"}, SuggestionSpan.FLAG_AUTO_CORRECTION);
169         setSuggestionSpan(suggestionSpan, text.indexOf('d'), text.indexOf('f') + 1);
170 
171         showSuggestionsPopup();
172 
173         assertSuggestionsPopupIsDisplayed();
174         assertSuggestionsPopupContainsItem("DEF");
175         assertSuggestionsPopupContainsItem("Def");
176         assertSuggestionsPopupContainsItem(
177                 getActivity().getString(com.android.internal.R.string.delete));
178 
179         // Select an item.
180         clickSuggestionsPopupItem("DEF");
181         assertSuggestionsPopupIsNotDisplayed();
182         onView(withId(R.id.textview)).check(matches(withText("abc DEF ghi")));
183 
184         showSuggestionsPopup();
185         assertSuggestionsPopupIsDisplayed();
186         assertSuggestionsPopupContainsItem("def");
187         assertSuggestionsPopupContainsItem("Def");
188         assertSuggestionsPopupContainsItem(
189                 getActivity().getString(com.android.internal.R.string.delete));
190 
191         // Delete
192         clickSuggestionsPopupItem(
193                 getActivity().getString(com.android.internal.R.string.delete));
194         assertSuggestionsPopupIsNotDisplayed();
195         onView(withId(R.id.textview)).check(matches(withText("abc ghi")));
196     }
197 
198     @Test
testMisspelled()199     public void testMisspelled() {
200         final String text = "abc def ghi";
201 
202         onView(withId(R.id.textview)).perform(click());
203         onView(withId(R.id.textview)).perform(replaceText(text));
204 
205         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
206                 new String[]{"DEF", "Def"}, SuggestionSpan.FLAG_MISSPELLED);
207         setSuggestionSpan(suggestionSpan, text.indexOf('d'), text.indexOf('f') + 1);
208 
209         showSuggestionsPopup();
210 
211         assertSuggestionsPopupIsDisplayed();
212         assertSuggestionsPopupContainsItem("DEF");
213         assertSuggestionsPopupContainsItem("Def");
214         assertSuggestionsPopupContainsItem(
215                 getActivity().getString(com.android.internal.R.string.addToDictionary));
216         assertSuggestionsPopupContainsItem(
217                 getActivity().getString(com.android.internal.R.string.delete));
218 
219         // Click "Add to dictionary".
220         clickSuggestionsPopupItem(
221                 getActivity().getString(com.android.internal.R.string.addToDictionary));
222         // TODO: Check if add to dictionary dialog is displayed.
223     }
224 
225     @Test
testEasyCorrect()226     public void testEasyCorrect() {
227         final String text = "abc def ghi";
228 
229         onView(withId(R.id.textview)).perform(click());
230         onView(withId(R.id.textview)).perform(replaceText(text));
231 
232         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
233                 new String[]{"DEF", "Def"},
234                 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
235         setSuggestionSpan(suggestionSpan, text.indexOf('d'), text.indexOf('f') + 1);
236 
237         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.indexOf('e')));
238 
239         assertSuggestionsPopupIsDisplayed();
240         assertSuggestionsPopupContainsItem("DEF");
241         assertSuggestionsPopupContainsItem("Def");
242         assertSuggestionsPopupContainsItem(
243                 getActivity().getString(com.android.internal.R.string.delete));
244 
245         // Select an item.
246         clickSuggestionsPopupItem("DEF");
247         assertSuggestionsPopupIsNotDisplayed();
248         onView(withId(R.id.textview)).check(matches(withText("abc DEF ghi")));
249 
250         onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.indexOf('e')));
251         assertSuggestionsPopupIsNotDisplayed();
252 
253         showSuggestionsPopup();
254         assertSuggestionsPopupIsDisplayed();
255         assertSuggestionsPopupContainsItem("def");
256         assertSuggestionsPopupContainsItem("Def");
257         assertSuggestionsPopupContainsItem(
258                 getActivity().getString(com.android.internal.R.string.delete));
259     }
260 
261     @Test
testTextAppearanceInSuggestionsPopup()262     public void testTextAppearanceInSuggestionsPopup() {
263         final String text = "abc def ghi";
264 
265         final String[] singleWordCandidates = {"DEF", "Def"};
266         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
267                 singleWordCandidates, SuggestionSpan.FLAG_MISSPELLED);
268         final String[] multiWordCandidates = {"ABC DEF GHI", "Abc Def Ghi"};
269         final SuggestionSpan multiWordSuggestionSpan = new SuggestionSpan(getActivity(),
270                 multiWordCandidates, SuggestionSpan.FLAG_MISSPELLED);
271 
272         final TypedArray array =
273                 getActivity().obtainStyledAttributes(com.android.internal.R.styleable.Theme);
274         final int id = array.getResourceId(
275                 com.android.internal.R.styleable.Theme_textEditSuggestionHighlightStyle, 0);
276         array.recycle();
277         final TextAppearanceSpan expectedSpan = new TextAppearanceSpan(getActivity(), id);
278         final TextPaint tmpTp = new TextPaint();
279         expectedSpan.updateDrawState(tmpTp);
280         final int expectedHighlightTextColor = tmpTp.getColor();
281         final float expectedHighlightTextSize = tmpTp.getTextSize();
282         final TextView textView = (TextView) getActivity().findViewById(R.id.textview);
283 
284         // In this test, the SuggestionsPopupWindow looks like
285         //   abc def ghi
286         // -----------------
287         // | abc *DEF* ghi |
288         // | abc *Def* ghi |
289         // | *ABC DEF GHI* |
290         // | *Abc Def Ghi* |
291         // -----------------
292         // | DELETE        |
293         // -----------------
294         // *XX* means that XX is highlighted.
295         for (int i = 0; i < 2; i++) {
296             onView(withId(R.id.textview)).perform(click());
297             onView(withId(R.id.textview)).perform(replaceText(text));
298             setSuggestionSpan(suggestionSpan, text.indexOf('d'), text.indexOf('f') + 1);
299             setSuggestionSpan(multiWordSuggestionSpan, 0, text.length());
300 
301             showSuggestionsPopup();
302             assertSuggestionsPopupIsDisplayed();
303             assertSuggestionsPopupContainsItem("abc DEF ghi");
304             assertSuggestionsPopupContainsItem("abc Def ghi");
305             assertSuggestionsPopupContainsItem("ABC DEF GHI");
306             assertSuggestionsPopupContainsItem("Abc Def Ghi");
307             assertSuggestionsPopupContainsItem(
308                     getActivity().getString(com.android.internal.R.string.delete));
309 
310             onSuggestionsPopup().check((view, e) -> {
311                 final ListView listView = view.findViewById(
312                         com.android.internal.R.id.suggestionContainer);
313                 assertNotNull(listView);
314                 final int childNum = listView.getChildCount();
315                 assertEquals(singleWordCandidates.length + multiWordCandidates.length, childNum);
316 
317                 for (int j = 0; j < childNum; j++) {
318                     final TextView suggestion = (TextView) listView.getChildAt(j);
319                     assertNotNull(suggestion);
320                     final Spanned spanned = (Spanned) suggestion.getText();
321                     assertNotNull(spanned);
322 
323                     // Check that the suggestion item order is kept.
324                     final String expectedText;
325                     if (j < singleWordCandidates.length) {
326                         expectedText = "abc " + singleWordCandidates[j] + " ghi";
327                     } else {
328                         expectedText = multiWordCandidates[j - singleWordCandidates.length];
329                     }
330                     assertEquals(expectedText, spanned.toString());
331 
332                     // Check that the text is highlighted with correct color and text size.
333                     final TextAppearanceSpan[] taSpan = spanned.getSpans(
334                             text.indexOf('d'), text.indexOf('f') + 1, TextAppearanceSpan.class);
335                     assertEquals(1, taSpan.length);
336                     TextPaint tp = new TextPaint();
337                     taSpan[0].updateDrawState(tp);
338                     assertEquals(expectedHighlightTextColor, tp.getColor());
339                     assertEquals(expectedHighlightTextSize, tp.getTextSize(), 0f);
340 
341                     // Check the correct part of the text is highlighted.
342                     final int expectedStart;
343                     final int expectedEnd;
344                     if (j < singleWordCandidates.length) {
345                         expectedStart = text.indexOf('d');
346                         expectedEnd = text.indexOf('f') + 1;
347                     } else {
348                         expectedStart = 0;
349                         expectedEnd = text.length();
350                     }
351                     assertEquals(expectedStart, spanned.getSpanStart(taSpan[0]));
352                     assertEquals(expectedEnd, spanned.getSpanEnd(taSpan[0]));
353                 }
354             });
355             pressBack();
356             onView(withId(R.id.textview))
357                     .inRoot(withDecorView(is(getActivity().getWindow().getDecorView())))
358                     .perform(clearText());
359         }
360     }
361 
362     @Test
testCursorVisibility()363     public void testCursorVisibility() {
364         final TextView textView = getActivity().findViewById(R.id.textview);
365         final String text = "abc";
366 
367         assertTrue(textView.isCursorVisible());
368 
369         onView(withId(R.id.textview)).perform(click());
370         onView(withId(R.id.textview)).perform(replaceText(text));
371         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
372                 new String[]{"ABC"}, SuggestionSpan.FLAG_AUTO_CORRECTION);
373         setSuggestionSpan(suggestionSpan, text.indexOf('a'), text.indexOf('c') + 1);
374         showSuggestionsPopup();
375 
376         assertSuggestionsPopupIsDisplayed();
377         assertSuggestionsPopupContainsItem("ABC");
378         assertFalse(textView.isCursorVisible());
379 
380         // Delete an item.
381         clickSuggestionsPopupItem(
382                 getActivity().getString(com.android.internal.R.string.delete));
383         assertSuggestionsPopupIsNotDisplayed();
384         assertTrue(textView.isCursorVisible());
385     }
386 
387     @Test
testCursorVisibilityWhenImeConsumesInput()388     public void testCursorVisibilityWhenImeConsumesInput() {
389         final TextView textView = getActivity().findViewById(R.id.textview);
390         final String text = "abc";
391 
392         assertTrue(textView.isCursorVisible());
393 
394         onView(withId(R.id.textview)).perform(click());
395         onView(withId(R.id.textview)).perform(replaceText(text));
396         setImeConsumesInputWithExpect(textView, true /* imeConsumesInput */,
397                 false /* expectedCursorVisibility */);
398         final SuggestionSpan suggestionSpan = new SuggestionSpan(getActivity(),
399                 new String[]{"ABC"}, SuggestionSpan.FLAG_AUTO_CORRECTION);
400         setSuggestionSpan(suggestionSpan, text.indexOf('a'), text.indexOf('c') + 1);
401         showSuggestionsPopup();
402 
403         assertSuggestionsPopupIsDisplayed();
404         assertSuggestionsPopupContainsItem("ABC");
405         assertFalse(textView.isCursorVisible());
406 
407         // Delete an item.
408         clickSuggestionsPopupItem(
409                 getActivity().getString(com.android.internal.R.string.delete));
410         assertSuggestionsPopupIsNotDisplayed();
411         assertFalse(textView.isCursorVisible());
412 
413         // Set IME not consumes input, cursor should be back to visible.
414         setImeConsumesInputWithExpect(textView, false /* imeConsumesInput */,
415                 true /* expectedCursorVisibility */);
416     }
417 
setImeConsumesInputWithExpect( final TextView textView, boolean imeConsumesInput, boolean expectedCursorVisibility)418     private void setImeConsumesInputWithExpect(
419             final TextView textView, boolean imeConsumesInput, boolean expectedCursorVisibility) {
420         textView.post(() -> textView.setImeConsumesInput(imeConsumesInput));
421         getInstrumentation().waitForIdleSync();
422         if (expectedCursorVisibility) {
423             assertTrue(textView.isCursorVisible());
424         } else {
425             assertFalse(textView.isCursorVisible());
426         }
427     }
428 }
429