1 /*
2  * Copyright (C) 2008 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.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotSame;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertSame;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assert.fail;
26 import static org.mockito.Mockito.mock;
27 
28 import android.app.Activity;
29 import android.app.AlertDialog;
30 import android.app.Dialog;
31 import android.app.Instrumentation;
32 import android.content.res.Resources;
33 import android.content.res.Resources.NotFoundException;
34 import android.content.res.Resources.Theme;
35 import android.graphics.Color;
36 import android.graphics.drawable.Drawable;
37 import android.view.ContextThemeWrapper;
38 import android.view.Gravity;
39 import android.view.KeyEvent;
40 import android.view.ViewGroup;
41 import android.widget.ArrayAdapter;
42 import android.widget.Spinner;
43 import android.widget.cts.util.TestUtils;
44 
45 import androidx.test.InstrumentationRegistry;
46 import androidx.test.annotation.UiThreadTest;
47 import androidx.test.filters.MediumTest;
48 import androidx.test.rule.ActivityTestRule;
49 import androidx.test.runner.AndroidJUnit4;
50 
51 import com.android.compatibility.common.util.CtsTouchUtils;
52 import com.android.compatibility.common.util.PollingCheck;
53 import com.android.compatibility.common.util.WidgetTestUtils;
54 
55 import org.junit.Before;
56 import org.junit.Rule;
57 import org.junit.Test;
58 import org.junit.runner.RunWith;
59 
60 /**
61  * Test {@link Spinner}.
62  */
63 @MediumTest
64 @RunWith(AndroidJUnit4.class)
65 public class SpinnerTest {
66     private Instrumentation mInstrumentation;
67     private Activity mActivity;
68     private Spinner mSpinnerDialogMode;
69     private Spinner mSpinnerDropdownMode;
70     private static final int SPINNER_HAS_FOCUS_DELAY_MS = 500;
71 
72     @Rule
73     public ActivityTestRule<SpinnerCtsActivity> mActivityRule =
74             new ActivityTestRule<>(SpinnerCtsActivity.class);
75 
76     @Before
setup()77     public void setup() {
78         mInstrumentation = InstrumentationRegistry.getInstrumentation();
79         mActivity = mActivityRule.getActivity();
80         mSpinnerDialogMode = (Spinner) mActivity.findViewById(R.id.spinner_dialog_mode);
81         mSpinnerDropdownMode = (Spinner) mActivity.findViewById(R.id.spinner_dropdown_mode);
82     }
83 
84     @Test
testConstructor()85     public void testConstructor() {
86         new Spinner(mActivity);
87 
88         new Spinner(mActivity, null);
89 
90         new Spinner(mActivity, null, android.R.attr.spinnerStyle);
91 
92         new Spinner(mActivity, Spinner.MODE_DIALOG);
93 
94         new Spinner(mActivity, Spinner.MODE_DROPDOWN);
95 
96         new Spinner(mActivity, null, android.R.attr.spinnerStyle, Spinner.MODE_DIALOG);
97 
98         new Spinner(mActivity, null, android.R.attr.spinnerStyle, Spinner.MODE_DROPDOWN);
99 
100         new Spinner(mActivity, null, 0, android.R.style.Widget_DeviceDefault_Spinner,
101                 Spinner.MODE_DIALOG);
102 
103         new Spinner(mActivity, null, 0, android.R.style.Widget_DeviceDefault_Spinner,
104                 Spinner.MODE_DROPDOWN);
105 
106         new Spinner(mActivity, null, 0, android.R.style.Widget_DeviceDefault_Light_Spinner,
107                 Spinner.MODE_DIALOG);
108 
109         new Spinner(mActivity, null, 0, android.R.style.Widget_DeviceDefault_Light_Spinner,
110                 Spinner.MODE_DROPDOWN);
111 
112         new Spinner(mActivity, null, 0, android.R.style.Widget_Material_Spinner,
113                 Spinner.MODE_DIALOG);
114 
115         new Spinner(mActivity, null, 0, android.R.style.Widget_Material_Spinner,
116                 Spinner.MODE_DROPDOWN);
117 
118         new Spinner(mActivity, null, 0, android.R.style.Widget_Material_Spinner_Underlined,
119                 Spinner.MODE_DIALOG);
120 
121         new Spinner(mActivity, null, 0, android.R.style.Widget_Material_Spinner_Underlined,
122                 Spinner.MODE_DROPDOWN);
123 
124         new Spinner(mActivity, null, 0, android.R.style.Widget_Material_Light_Spinner,
125                 Spinner.MODE_DIALOG);
126 
127         new Spinner(mActivity, null, 0, android.R.style.Widget_Material_Light_Spinner,
128                 Spinner.MODE_DROPDOWN);
129 
130         new Spinner(mActivity, null, 0, android.R.style.Widget_Material_Light_Spinner_Underlined,
131                 Spinner.MODE_DIALOG);
132 
133         new Spinner(mActivity, null, 0, android.R.style.Widget_Material_Light_Spinner_Underlined,
134                 Spinner.MODE_DROPDOWN);
135 
136         final Resources.Theme popupTheme = mActivity.getResources().newTheme();
137         popupTheme.applyStyle(android.R.style.Theme_Material, true);
138 
139         new Spinner(mActivity, null, android.R.attr.spinnerStyle, 0, Spinner.MODE_DIALOG,
140                 popupTheme);
141 
142         new Spinner(mActivity, null, android.R.attr.spinnerStyle, 0, Spinner.MODE_DROPDOWN,
143                 popupTheme);
144     }
145 
verifyGetBaseline(Spinner spinner)146     private void verifyGetBaseline(Spinner spinner) throws Throwable {
147         assertEquals(-1, spinner.getBaseline());
148 
149         ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(mActivity,
150                 R.array.string, android.R.layout.simple_spinner_item);
151         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
152         mActivityRule.runOnUiThread(() -> {
153             spinner.setAdapter(adapter);
154             assertTrue(spinner.getBaseline() > 0);
155         });
156     }
157 
158     @Test
testGetBaseline()159     public void testGetBaseline() throws Throwable {
160         verifyGetBaseline(mSpinnerDialogMode);
161         verifyGetBaseline(mSpinnerDropdownMode);
162     }
163 
verifySetOnItemClickListener(Spinner spinner)164     private void verifySetOnItemClickListener(Spinner spinner) {
165         try {
166             spinner.setOnItemClickListener(null);
167             fail("Should throw RuntimeException");
168         } catch (RuntimeException e) {
169         }
170 
171         try {
172             spinner.setOnItemClickListener(mock(Spinner.OnItemClickListener.class));
173             fail("Should throw RuntimeException");
174         } catch (RuntimeException e) {
175         }
176     }
177 
178     @Test
testSetOnItemClickListener()179     public void testSetOnItemClickListener() {
180         verifySetOnItemClickListener(mSpinnerDialogMode);
181         verifySetOnItemClickListener(mSpinnerDropdownMode);
182     }
183 
verifyPerformClick(Spinner spinner)184     private void verifyPerformClick(Spinner spinner) throws Throwable {
185         mActivityRule.runOnUiThread(() -> assertTrue(spinner.performClick()));
186     }
187 
188     @Test
testPerformClick()189     public void testPerformClick() throws Throwable {
190         verifyPerformClick(mSpinnerDialogMode);
191         verifyPerformClick(mSpinnerDropdownMode);
192     }
193 
verifyOnClick(Spinner spinner)194     private void verifyOnClick(Spinner spinner) {
195         // normal value
196         AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
197         AlertDialog alertDialog = builder.show();
198         assertTrue(alertDialog.isShowing());
199 
200         spinner.onClick(alertDialog, 10);
201         assertEquals(10, spinner.getSelectedItemPosition());
202         assertFalse(alertDialog.isShowing());
203 
204         // exceptional
205         try {
206             spinner.onClick(null, 10);
207             fail("did not throw NullPointerException");
208         } catch (NullPointerException e) {
209         }
210 
211         Dialog dialog = new Dialog(mActivity);
212         dialog.show();
213         assertTrue(dialog.isShowing());
214 
215         spinner.onClick(dialog, -10);
216         assertEquals(-10, spinner.getSelectedItemPosition());
217         assertFalse(dialog.isShowing());
218     }
219 
220     @UiThreadTest
221     @Test
testOnClick()222     public void testOnClick() {
223         verifyOnClick(mSpinnerDialogMode);
224         verifyOnClick(mSpinnerDropdownMode);
225     }
226 
verifyAccessPrompt(Spinner spinner)227     private void verifyAccessPrompt(Spinner spinner) throws Throwable {
228         final String initialPrompt = mActivity.getString(R.string.text_view_hello);
229         assertEquals(initialPrompt, spinner.getPrompt());
230 
231         final String promptText = "prompt text";
232 
233         mActivityRule.runOnUiThread(() -> spinner.setPrompt(promptText));
234         assertEquals(promptText, spinner.getPrompt());
235 
236         spinner.setPrompt(null);
237         assertNull(spinner.getPrompt());
238     }
239 
240     @Test
testAccessPrompt()241     public void testAccessPrompt() throws Throwable {
242         verifyAccessPrompt(mSpinnerDialogMode);
243         verifyAccessPrompt(mSpinnerDropdownMode);
244     }
245 
verifySetPromptId(Spinner spinner)246     private void verifySetPromptId(Spinner spinner) throws Throwable {
247         mActivityRule.runOnUiThread(() -> spinner.setPromptId(R.string.hello_world));
248         assertEquals(mActivity.getString(R.string.hello_world), spinner.getPrompt());
249 
250         try {
251             spinner.setPromptId(-1);
252             fail("Should throw NotFoundException");
253         } catch (NotFoundException e) {
254             // issue 1695243, not clear what is supposed to happen if promptId is exceptional.
255         }
256 
257         try {
258             spinner.setPromptId(Integer.MAX_VALUE);
259             fail("Should throw NotFoundException");
260         } catch (NotFoundException e) {
261             // issue 1695243, not clear what is supposed to happen if promptId is exceptional.
262         }
263     }
264 
265     @Test
testSetPromptId()266     public void testSetPromptId() throws Throwable {
267         verifySetPromptId(mSpinnerDialogMode);
268         verifySetPromptId(mSpinnerDropdownMode);
269     }
270 
271     @UiThreadTest
272     @Test
testGetPopupContext()273     public void testGetPopupContext() {
274         Theme theme = mActivity.getResources().newTheme();
275         Spinner themeSpinner = new Spinner(mActivity, null,
276                 android.R.attr.spinnerStyle, 0, Spinner.MODE_DIALOG, theme);
277         assertNotSame(mActivity, themeSpinner.getPopupContext());
278         assertSame(theme, themeSpinner.getPopupContext().getTheme());
279 
280         ContextThemeWrapper context = (ContextThemeWrapper)themeSpinner.getPopupContext();
281         assertSame(mActivity, context.getBaseContext());
282     }
283 
verifyGravity(Spinner spinner)284     private void verifyGravity(Spinner spinner) throws Throwable {
285         // Note that here we're using a custom layout for the spinner's selected item
286         // that doesn't span the whole width of the parent. That way we're exercising the
287         // relevant path in spinner's layout pass that handles the currently set gravity
288         ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(mActivity,
289                 R.array.string, R.layout.simple_spinner_item_layout);
290         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
291         mActivityRule.runOnUiThread(() -> spinner.setAdapter(adapter));
292 
293         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, spinner, () -> {
294             spinner.setSelection(1);
295             spinner.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
296             spinner.requestLayout();
297         });
298 
299         mActivityRule.runOnUiThread(() -> spinner.setGravity(Gravity.LEFT));
300         assertEquals(Gravity.LEFT, spinner.getGravity());
301 
302         mActivityRule.runOnUiThread(() -> spinner.setGravity(Gravity.CENTER_HORIZONTAL));
303         assertEquals(Gravity.CENTER_HORIZONTAL, spinner.getGravity());
304 
305         mActivityRule.runOnUiThread((() -> spinner.setGravity(Gravity.RIGHT)));
306         assertEquals(Gravity.RIGHT, spinner.getGravity());
307 
308         mActivityRule.runOnUiThread(() -> spinner.setGravity(Gravity.START));
309         assertEquals(Gravity.START, spinner.getGravity());
310 
311         mActivityRule.runOnUiThread(() -> spinner.setGravity(Gravity.END));
312         assertEquals(Gravity.END, spinner.getGravity());
313     }
314 
315     @Test
testGravity()316     public void testGravity() throws Throwable {
317         verifyGravity(mSpinnerDialogMode);
318         verifyGravity(mSpinnerDropdownMode);
319     }
320 
321     @Test
testDropDownMetricsDropdownMode()322     public void testDropDownMetricsDropdownMode() throws Throwable {
323         ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(mActivity,
324                 R.array.string, android.R.layout.simple_spinner_item);
325         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
326         mActivityRule.runOnUiThread(() -> mSpinnerDropdownMode.setAdapter(adapter));
327 
328         final Resources res = mActivity.getResources();
329         final int dropDownWidth = res.getDimensionPixelSize(R.dimen.spinner_dropdown_width);
330         final int dropDownOffsetHorizontal =
331                 res.getDimensionPixelSize(R.dimen.spinner_dropdown_offset_h);
332         final int dropDownOffsetVertical =
333                 res.getDimensionPixelSize(R.dimen.spinner_dropdown_offset_v);
334 
335         mActivityRule.runOnUiThread(() -> {
336             mSpinnerDropdownMode.setDropDownWidth(dropDownWidth);
337             mSpinnerDropdownMode.setDropDownHorizontalOffset(dropDownOffsetHorizontal);
338             mSpinnerDropdownMode.setDropDownVerticalOffset(dropDownOffsetVertical);
339         });
340 
341         // Use instrumentation to emulate a tap on the spinner to bring down its popup
342         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSpinnerDropdownMode);
343         // Verify that we're showing the popup
344         PollingCheck.waitFor(() -> mSpinnerDropdownMode.isPopupShowing());
345 
346         // And test its attributes
347         assertEquals(dropDownWidth, mSpinnerDropdownMode.getDropDownWidth());
348         // TODO: restore when b/28089349 is addressed
349         // assertEquals(dropDownOffsetHorizontal,
350         //      mSpinnerDropdownMode.getDropDownHorizontalOffset());
351         assertEquals(dropDownOffsetVertical, mSpinnerDropdownMode.getDropDownVerticalOffset());
352     }
353 
354     @Test
testDropDownMetricsDialogMode()355     public void testDropDownMetricsDialogMode() throws Throwable {
356         ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(mActivity,
357                 R.array.string, android.R.layout.simple_spinner_item);
358         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
359         mActivityRule.runOnUiThread(() -> mSpinnerDialogMode.setAdapter(adapter));
360 
361         final Resources res = mActivity.getResources();
362         final int dropDownWidth = res.getDimensionPixelSize(R.dimen.spinner_dropdown_width);
363         final int dropDownOffsetHorizontal =
364                 res.getDimensionPixelSize(R.dimen.spinner_dropdown_offset_h);
365         final int dropDownOffsetVertical =
366                 res.getDimensionPixelSize(R.dimen.spinner_dropdown_offset_v);
367 
368         mActivityRule.runOnUiThread(() -> {
369             // These are all expected to be no-ops
370             mSpinnerDialogMode.setDropDownWidth(dropDownWidth);
371             mSpinnerDialogMode.setDropDownHorizontalOffset(dropDownOffsetHorizontal);
372             mSpinnerDialogMode.setDropDownVerticalOffset(dropDownOffsetVertical);
373         });
374 
375         // Use instrumentation to emulate a tap on the spinner to bring down its popup
376         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, mSpinnerDialogMode);
377         // Verify that we're showing the popup
378         PollingCheck.waitFor(() -> mSpinnerDialogMode.isPopupShowing());
379 
380         // And test its attributes. Note that we are not testing the result of getDropDownWidth
381         // for this mode
382         assertEquals(0, mSpinnerDialogMode.getDropDownHorizontalOffset());
383         assertEquals(0, mSpinnerDialogMode.getDropDownVerticalOffset());
384     }
385 
386     @Test
testDropDownBackgroundDropdownMode()387     public void testDropDownBackgroundDropdownMode() throws Throwable {
388         ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(mActivity,
389                 R.array.string, android.R.layout.simple_spinner_item);
390         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
391         mActivityRule.runOnUiThread(() -> mSpinnerDropdownMode.setAdapter(adapter));
392 
393         // Set blue background on the popup
394         mActivityRule.runOnUiThread(() ->
395                 mSpinnerDropdownMode.setPopupBackgroundResource(R.drawable.blue_fill));
396 
397         // Use instrumentation to emulate a tap on the spinner to bring down its popup
398         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule, mSpinnerDropdownMode);
399         // Verify that we're showing the popup
400         PollingCheck.waitFor(() -> mSpinnerDropdownMode.isPopupShowing());
401         // And test its fill
402         Drawable dropDownBackground = mSpinnerDropdownMode.getPopupBackground();
403         TestUtils.assertAllPixelsOfColor("Drop down should be blue", dropDownBackground,
404                 dropDownBackground.getBounds().width(), dropDownBackground.getBounds().height(),
405                 false, Color.BLUE, 1, true);
406         waitForHasFocusMS(SPINNER_HAS_FOCUS_DELAY_MS);
407         // Dismiss the popup with the emulated back key
408         mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
409         // Verify that we're not showing the popup
410         PollingCheck.waitFor(() -> !mSpinnerDropdownMode.isPopupShowing());
411 
412         // Set yellow background on the popup
413         mActivityRule.runOnUiThread(() ->
414                 mSpinnerDropdownMode.setPopupBackgroundDrawable(
415                         mActivity.getDrawable(R.drawable.yellow_fill)));
416 
417         // Use instrumentation to emulate a tap on the spinner to bring down its popup
418         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mSpinnerDropdownMode, () -> {
419             mSpinnerDropdownMode.performClick();
420         });
421         // Verify that we're showing the popup
422         PollingCheck.waitFor(() -> mSpinnerDropdownMode.isPopupShowing());
423         // And test its fill
424         dropDownBackground = mSpinnerDropdownMode.getPopupBackground();
425         TestUtils.assertAllPixelsOfColor("Drop down should be yellow", dropDownBackground,
426                 dropDownBackground.getBounds().width(), dropDownBackground.getBounds().height(),
427                 false, Color.YELLOW, 1, true);
428     }
429 
430     @Test
testDropDownBackgroundDialogMode()431     public void testDropDownBackgroundDialogMode() throws Throwable {
432         ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(mActivity,
433                 R.array.string, android.R.layout.simple_spinner_item);
434         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
435         mActivityRule.runOnUiThread(() -> mSpinnerDialogMode.setAdapter(adapter));
436 
437         // Set blue background on the popup
438         mActivityRule.runOnUiThread(() ->
439                 mSpinnerDialogMode.setPopupBackgroundResource(R.drawable.blue_fill));
440 
441         // Use instrumentation to emulate a tap on the spinner to bring down its popup
442         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, mSpinnerDialogMode);
443         // Verify that we're showing the popup
444         PollingCheck.waitFor(() -> mSpinnerDialogMode.isPopupShowing());
445         // And test that getPopupBackground returns null
446         assertNull(mSpinnerDialogMode.getPopupBackground());
447 
448         // Dismiss the popup with the emulated back key
449         mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
450         // Verify that we're not showing the popup
451         PollingCheck.waitFor(() -> !mSpinnerDropdownMode.isPopupShowing());
452 
453         // Set yellow background on the popup
454         mActivityRule.runOnUiThread(() ->
455                 mSpinnerDialogMode.setPopupBackgroundDrawable(
456                         mActivity.getDrawable(R.drawable.yellow_fill)));
457 
458         // Use instrumentation to emulate a tap on the spinner to bring down its popup
459         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, mSpinnerDialogMode);
460         // Verify that we're showing the popup
461         PollingCheck.waitFor(() -> mSpinnerDialogMode.isPopupShowing());
462         // And test that getPopupBackground returns null
463         assertNull(mSpinnerDialogMode.getPopupBackground());
464     }
465 
waitForHasFocusMS(int milliseconds)466     private void waitForHasFocusMS(int milliseconds) {
467         try {
468             Thread.sleep(milliseconds);
469         } catch (InterruptedException e) {
470             fail("unexpected InterruptedException : "+ e);
471         }
472 
473     }
474 }
475