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 */
16package android.support.v7.app;
17
18import static android.support.test.espresso.Espresso.onData;
19import static android.support.test.espresso.Espresso.onView;
20import static android.support.test.espresso.action.ViewActions.click;
21import static android.support.test.espresso.assertion.PositionAssertions.isBelow;
22import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
23import static android.support.test.espresso.assertion.ViewAssertions.matches;
24import static android.support.test.espresso.matcher.LayoutMatchers.hasEllipsizedText;
25import static android.support.test.espresso.matcher.RootMatchers.isDialog;
26import static android.support.test.espresso.matcher.ViewMatchers.hasSibling;
27import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
28import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
29import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
30import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
31import static android.support.test.espresso.matcher.ViewMatchers.withId;
32import static android.support.test.espresso.matcher.ViewMatchers.withText;
33
34import static org.hamcrest.Matchers.instanceOf;
35import static org.hamcrest.Matchers.not;
36import static org.hamcrest.core.AllOf.allOf;
37import static org.hamcrest.core.Is.is;
38import static org.junit.Assert.assertEquals;
39import static org.junit.Assert.assertFalse;
40import static org.junit.Assert.assertNotNull;
41import static org.junit.Assert.assertNull;
42import static org.junit.Assert.assertTrue;
43import static org.mockito.ArgumentMatchers.anyLong;
44import static org.mockito.Mockito.any;
45import static org.mockito.Mockito.mock;
46import static org.mockito.Mockito.never;
47import static org.mockito.Mockito.times;
48import static org.mockito.Mockito.verify;
49
50import android.content.Context;
51import android.content.DialogInterface;
52import android.graphics.drawable.ColorDrawable;
53import android.os.Handler;
54import android.os.Message;
55import android.support.annotation.ColorInt;
56import android.support.annotation.StringRes;
57import android.support.test.annotation.UiThreadTest;
58import android.support.test.espresso.Espresso;
59import android.support.test.espresso.ViewInteraction;
60import android.support.test.filters.LargeTest;
61import android.support.test.filters.MediumTest;
62import android.support.test.filters.SmallTest;
63import android.support.test.rule.ActivityTestRule;
64import android.support.v7.appcompat.test.R;
65import android.support.v7.testutils.TestUtilsMatchers;
66import android.text.TextUtils;
67import android.util.TypedValue;
68import android.view.LayoutInflater;
69import android.view.View;
70import android.widget.ArrayAdapter;
71import android.widget.Button;
72import android.widget.CheckedTextView;
73import android.widget.ImageView;
74import android.widget.ListAdapter;
75import android.widget.ListView;
76
77import org.hamcrest.Matcher;
78import org.junit.After;
79import org.junit.Before;
80import org.junit.Rule;
81import org.junit.Test;
82import org.mockito.ArgumentCaptor;
83
84/**
85 * Tests in this class make a few assumptions about the underlying implementation of
86 * <code>AlertDialog</code>. While the assumptions don't go all the way down to individual
87 * <code>R.id</code> references or very specific layout arrangements, internal refactoring
88 * of <code>AlertDialog</code> might require corresponding restructuring of the matching
89 * tests. Specifically:
90 *
91 * <ul>
92 *     <li>Testing <code>setIcon</code> API assumes that the icon is displayed by a separate
93 *     <code>ImageView</code> which is a sibling of a title view.</li>
94 *     <li>Testing <code>setMultiChoiceItems</code> API assumes that each item in the list
95 *     is rendered by a single <code>CheckedTextView</code>.</li>
96 *     <li>Testing <code>setSingleChoiceItems</code> API assumes that each item in the list
97 *     is rendered by a single <code>CheckedTextView</code>.</li>
98 * </ul>
99 */
100public class AlertDialogTest {
101    @Rule
102    public final ActivityTestRule<AlertDialogTestActivity> mActivityTestRule;
103
104    private Button mButton;
105
106    private AlertDialog mAlertDialog;
107
108    public AlertDialogTest() {
109        mActivityTestRule = new ActivityTestRule<>(AlertDialogTestActivity.class);
110    }
111
112    @Before
113    public void setUp() {
114        final AlertDialogTestActivity activity = mActivityTestRule.getActivity();
115        mButton = (Button) activity.findViewById(R.id.test_button);
116    }
117
118    @After
119    public void tearDown() throws Throwable {
120        if ((mAlertDialog != null) && mAlertDialog.isShowing()) {
121            mActivityTestRule.runOnUiThread(new Runnable() {
122                @Override
123                public void run() {
124                    mAlertDialog.hide();
125                }
126            });
127        }
128    }
129
130    private void wireBuilder(final AlertDialog.Builder builder) {
131        mButton.setOnClickListener(new View.OnClickListener() {
132            @Override
133            public void onClick(View v) {
134                mAlertDialog = builder.show();
135            }
136        });
137    }
138
139    @Test
140    @SmallTest
141    @UiThreadTest
142    public void testBuilderTheme() {
143        final Context context = mActivityTestRule.getActivity();
144        final AlertDialog dialog = new AlertDialog.Builder(context, R.style.Theme_TextColors)
145                .setTitle(R.string.alert_dialog_title)
146                .setMessage(R.string.alert_dialog_content)
147                .create();
148
149        final TypedValue tv = new TypedValue();
150        dialog.getContext().getTheme().resolveAttribute(android.R.attr.textColorPrimary, tv, true);
151        assertEquals(0xFF0000FF, tv.data);
152    }
153
154    @Test
155    @MediumTest
156    public void testBasicContent() {
157        final Context context = mActivityTestRule.getActivity();
158        AlertDialog.Builder builder = new AlertDialog.Builder(context)
159                .setTitle(R.string.alert_dialog_title)
160                .setMessage(R.string.alert_dialog_content);
161        wireBuilder(builder);
162
163        onView(withId(R.id.test_button)).perform(click());
164
165        // Test that we're showing a dialog with vertically stacked title and content
166        final String expectedTitle = context.getString(R.string.alert_dialog_title);
167        final String expectedMessage = context.getString(R.string.alert_dialog_content);
168        onView(withText(expectedTitle)).inRoot(isDialog()).check(matches(isDisplayed()));
169        onView(withText(expectedMessage)).inRoot(isDialog()).check(matches(isDisplayed()));
170        onView(withText(expectedMessage)).inRoot(isDialog()).check(
171                isBelow(withText(expectedTitle)));
172
173        assertNull("No list view", mAlertDialog.getListView());
174
175        assertEquals("Positive button not shown", View.GONE,
176                mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility());
177        assertEquals("Negative button not shown", View.GONE,
178                mAlertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).getVisibility());
179        assertEquals("Neutral button not shown", View.GONE,
180                mAlertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).getVisibility());
181    }
182
183    // Tests for message logic
184
185    @Test
186    @MediumTest
187    public void testMessageString() {
188        final String dialogMessage = "Dialog message";
189        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
190                .setTitle(R.string.alert_dialog_title)
191                .setMessage(dialogMessage);
192        wireBuilder(builder);
193
194        onView(withId(R.id.test_button)).perform(click());
195        onView(withText(dialogMessage)).inRoot(isDialog()).check(matches(isDisplayed()));
196    }
197
198    @Test
199    @MediumTest
200    public void testMessageStringPostCreation() throws Throwable {
201        final String dialogInitialMessage = "Initial message";
202        final String dialogUpdatedMessage = "Updated message";
203        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
204                .setTitle(R.string.alert_dialog_title)
205                .setMessage(dialogInitialMessage);
206        wireBuilder(builder);
207
208        // Click the button to show the dialog and check that it shows the initial message
209        onView(withId(R.id.test_button)).perform(click());
210        onView(withText(dialogInitialMessage)).inRoot(isDialog()).check(matches(isDisplayed()));
211
212        // Update the dialog message
213        mActivityTestRule.runOnUiThread(new Runnable() {
214            @Override
215            public void run() {
216                mAlertDialog.setMessage(dialogUpdatedMessage);
217            }
218        });
219        // Check that the old message is not showing
220        onView(withText(dialogInitialMessage)).inRoot(isDialog()).check(doesNotExist());
221        // and that the new message is showing
222        onView(withText(dialogUpdatedMessage)).inRoot(isDialog()).check(matches(isDisplayed()));
223    }
224
225    // Tests for custom title logic
226
227    /**
228     * Helper method to verify that setting custom title hides the default title and shows
229     * the custom title above the dialog message.
230     */
231    private void verifyCustomTitle() {
232        final Context context = mActivityTestRule.getActivity();
233
234        // Test that we're showing a dialog with vertically stacked custom title and content
235        final String title = context.getString(R.string.alert_dialog_title);
236        final String expectedCustomTitle = context.getString(R.string.alert_dialog_custom_title);
237        final String expectedMessage = context.getString(R.string.alert_dialog_content);
238
239        // Check that the default title is not showing
240        onView(withText(title)).inRoot(isDialog()).check(doesNotExist());
241        // Check that the custom title is fully displayed with no text eliding and is
242        // stacked above the message
243        onView(withText(expectedCustomTitle)).inRoot(isDialog()).check(
244                matches(isCompletelyDisplayed()));
245        onView(withText(expectedCustomTitle)).inRoot(isDialog()).check(
246                matches(not(hasEllipsizedText())));
247        onView(withText(expectedMessage)).inRoot(isDialog()).check(matches(isDisplayed()));
248        onView(withText(expectedMessage)).inRoot(isDialog()).check(
249                isBelow(withText(expectedCustomTitle)));
250    }
251
252    @Test
253    @MediumTest
254    public void testCustomTitle() {
255        final Context context = mActivityTestRule.getActivity();
256        final LayoutInflater inflater = LayoutInflater.from(context);
257        AlertDialog.Builder builder = new AlertDialog.Builder(context)
258                .setTitle(R.string.alert_dialog_title)
259                .setMessage(R.string.alert_dialog_content)
260                .setCustomTitle(inflater.inflate(R.layout.alert_dialog_custom_title, null, false));
261        wireBuilder(builder);
262
263        onView(withId(R.id.test_button)).perform(click());
264
265        verifyCustomTitle();
266    }
267
268    @Test
269    @MediumTest
270    public void testCustomTitlePostCreation() {
271        final Context context = mActivityTestRule.getActivity();
272        final LayoutInflater inflater = LayoutInflater.from(context);
273        final AlertDialog.Builder builder = new AlertDialog.Builder(context)
274                .setTitle(R.string.alert_dialog_title)
275                .setMessage(R.string.alert_dialog_content);
276
277        mButton.setOnClickListener(new View.OnClickListener() {
278            @Override
279            public void onClick(View v) {
280                mAlertDialog = builder.create();
281
282                // Configure custom title
283                mAlertDialog.setCustomTitle(inflater.inflate(
284                        R.layout.alert_dialog_custom_title, null, false));
285
286                mAlertDialog.show();
287            }
288        });
289
290        // Click the button to create the dialog, configure custom title and show the dialog
291        onView(withId(R.id.test_button)).perform(click());
292
293        verifyCustomTitle();
294    }
295
296    // Tests for custom view logic
297
298    /**
299     * Helper method to verify that setting custom view shows the content of that view.
300     */
301    private void verifyCustomView() {
302        final Context context = mActivityTestRule.getActivity();
303
304        // Test that we're showing a dialog with vertically stacked custom title and content
305        final String expectedCustomText1 = context.getString(R.string.alert_dialog_custom_text1);
306        final String expectedCustomText2 = context.getString(R.string.alert_dialog_custom_text2);
307
308        // Check that we're showing the content of our custom view
309        onView(withId(R.id.alert_dialog_custom_view)).inRoot(isDialog()).check(
310                matches(isCompletelyDisplayed()));
311        onView(withText(expectedCustomText1)).inRoot(isDialog()).check(
312                matches(isCompletelyDisplayed()));
313        onView(withText(expectedCustomText1)).inRoot(isDialog()).check(
314                matches(not(hasEllipsizedText())));
315        onView(withText(expectedCustomText2)).inRoot(isDialog()).check(
316                matches(isCompletelyDisplayed()));
317        onView(withText(expectedCustomText2)).inRoot(isDialog()).check(
318                matches(not(hasEllipsizedText())));
319    }
320
321    @Test
322    @MediumTest
323    public void testCustomView() {
324        final Context context = mActivityTestRule.getActivity();
325        final LayoutInflater inflater = LayoutInflater.from(context);
326        AlertDialog.Builder builder = new AlertDialog.Builder(context)
327                .setTitle(R.string.alert_dialog_title)
328                .setMessage(R.string.alert_dialog_content)
329                .setView(inflater.inflate(R.layout.alert_dialog_custom_view, null, false));
330        wireBuilder(builder);
331
332        onView(withId(R.id.test_button)).perform(click());
333
334        verifyCustomView();
335    }
336
337    @Test
338    @MediumTest
339    public void testCustomViewById() {
340        final Context context = mActivityTestRule.getActivity();
341        AlertDialog.Builder builder = new AlertDialog.Builder(context)
342                .setTitle(R.string.alert_dialog_title)
343                .setMessage(R.string.alert_dialog_content)
344                .setView(R.layout.alert_dialog_custom_view);
345        wireBuilder(builder);
346
347        onView(withId(R.id.test_button)).perform(click());
348
349        verifyCustomView();
350    }
351
352    @Test
353    @MediumTest
354    public void testCustomViewPostCreation() {
355        final Context context = mActivityTestRule.getActivity();
356        final LayoutInflater inflater = LayoutInflater.from(context);
357        final AlertDialog.Builder builder = new AlertDialog.Builder(context)
358                .setTitle(R.string.alert_dialog_title)
359                .setMessage(R.string.alert_dialog_content);
360
361        mButton.setOnClickListener(new View.OnClickListener() {
362            @Override
363            public void onClick(View v) {
364                mAlertDialog = builder.create();
365
366                // Configure custom view
367                mAlertDialog.setView(inflater.inflate(
368                        R.layout.alert_dialog_custom_view, null, false));
369
370                mAlertDialog.show();
371            }
372        });
373
374        // Click the button to create the dialog, configure custom view and show the dialog
375        onView(withId(R.id.test_button)).perform(click());
376
377        verifyCustomView();
378    }
379
380    // Tests for cancel logic
381
382    @Test
383    @MediumTest
384    public void testCancelCancelableDialog() {
385        DialogInterface.OnCancelListener mockCancelListener =
386                mock(DialogInterface.OnCancelListener.class);
387        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
388                .setTitle(R.string.alert_dialog_title)
389                .setMessage(R.string.alert_dialog_content)
390                .setCancelable(true)
391                .setOnCancelListener(mockCancelListener);
392        wireBuilder(builder);
393
394        onView(withId(R.id.test_button)).perform(click());
395
396        // Emulate a tap on the device BACK button
397        Espresso.pressBack();
398
399        // Since our dialog is cancelable, check that the cancel listener has been invoked
400        verify(mockCancelListener, times(1)).onCancel(mAlertDialog);
401    }
402
403    @Test
404    @MediumTest
405    public void testCancelNonCancelableDialog() {
406        DialogInterface.OnCancelListener mockCancelListener =
407                mock(DialogInterface.OnCancelListener.class);
408        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
409                .setTitle(R.string.alert_dialog_title)
410                .setMessage(R.string.alert_dialog_content)
411                .setCancelable(false)
412                .setOnCancelListener(mockCancelListener);
413        wireBuilder(builder);
414
415        onView(withId(R.id.test_button)).perform(click());
416
417        // Emulate a tap on the device BACK button
418        Espresso.pressBack();
419
420        // Since our dialog is not cancelable, check that the cancel listener has not been invoked
421        verify(mockCancelListener, never()).onCancel(mAlertDialog);
422    }
423
424    // Tests for items content logic (simple, single-choice, multi-choice)
425
426    private void verifySimpleItemsContent(String[] expectedContent,
427            DialogInterface.OnClickListener onClickListener) {
428        final int expectedCount = expectedContent.length;
429
430        onView(withId(R.id.test_button)).perform(click());
431
432        final ListView listView = mAlertDialog.getListView();
433        assertNotNull("List view is shown", listView);
434
435        final ListAdapter listAdapter = listView.getAdapter();
436        assertEquals("List has " + expectedCount + " entries",
437                expectedCount, listAdapter.getCount());
438        for (int i = 0; i < expectedCount; i++) {
439            assertEquals("List entry #" + i, expectedContent[i], listAdapter.getItem(i));
440        }
441
442        // Test that all items are showing
443        onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
444        for (int i = 0; i < expectedCount; i++) {
445            onData(allOf(is(instanceOf(String.class)), is(expectedContent[i]))).inRoot(isDialog()).
446                    check(matches(isDisplayed()));
447        }
448
449        // Verify that our click listener hasn't been called yet
450        verify(onClickListener, never()).onClick(any(DialogInterface.class), any(int.class));
451        // Test that a click on an item invokes the registered listener
452        int indexToClick = expectedCount - 2;
453        onData(allOf(is(instanceOf(String.class)), is(expectedContent[indexToClick]))).
454                inRoot(isDialog()).perform(click());
455        verify(onClickListener, times(1)).onClick(mAlertDialog, indexToClick);
456    }
457
458    @Test
459    @MediumTest
460    public void testCustomAdapter() {
461        final Context context = mActivityTestRule.getActivity();
462        final String[] content = context.getResources().getStringArray(R.array.alert_dialog_items);
463        final DialogInterface.OnClickListener mockClickListener =
464                mock(DialogInterface.OnClickListener.class);
465        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
466                .setTitle(R.string.alert_dialog_title)
467                .setAdapter(
468                        new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, content),
469                        mockClickListener);
470        wireBuilder(builder);
471
472        verifySimpleItemsContent(content, mockClickListener);
473    }
474
475    @Test
476    @MediumTest
477    public void testSimpleItemsFromRuntimeArray() {
478        final String[] content = new String[] { "Alice", "Bob", "Charlie", "Delta" };
479        final DialogInterface.OnClickListener mockClickListener =
480                mock(DialogInterface.OnClickListener.class);
481        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
482                .setTitle(R.string.alert_dialog_title)
483                .setItems(content, mockClickListener);
484        wireBuilder(builder);
485
486        verifySimpleItemsContent(content, mockClickListener);
487    }
488
489    @Test
490    @MediumTest
491    public void testSimpleItemsFromResourcesArray() {
492        final DialogInterface.OnClickListener mockClickListener =
493                mock(DialogInterface.OnClickListener.class);
494        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
495                .setTitle(R.string.alert_dialog_title)
496                .setItems(R.array.alert_dialog_items, mockClickListener);
497        wireBuilder(builder);
498
499        verifySimpleItemsContent(mActivityTestRule.getActivity().getResources().getStringArray(
500                R.array.alert_dialog_items), mockClickListener);
501    }
502
503    /**
504     * Helper method to verify the state of the multi-choice items list. It gets the String
505     * array of content and verifies that:
506     *
507     * 1. The items in the array are rendered as CheckedTextViews inside a ListView
508     * 2. Each item in the array is displayed
509     * 3. Checked state of each row in the ListView corresponds to the matching entry in the
510     *    passed boolean array
511     */
512    private void verifyMultiChoiceItemsState(String[] expectedContent,
513            boolean[] checkedTracker) {
514        final int expectedCount = expectedContent.length;
515
516        final ListView listView = mAlertDialog.getListView();
517        assertNotNull("List view is shown", listView);
518
519        final ListAdapter listAdapter = listView.getAdapter();
520        assertEquals("List has " + expectedCount + " entries",
521                expectedCount, listAdapter.getCount());
522        for (int i = 0; i < expectedCount; i++) {
523            assertEquals("List entry #" + i, expectedContent[i], listAdapter.getItem(i));
524        }
525
526        for (int i = 0; i < expectedCount; i++) {
527            Matcher checkedStateMatcher = checkedTracker[i] ? TestUtilsMatchers.isCheckedTextView() :
528                    TestUtilsMatchers.isNonCheckedTextView();
529            // Check that the corresponding row is rendered as CheckedTextView with expected
530            // checked state.
531            onData(allOf(is(instanceOf(String.class)), is(expectedContent[i]))).inRoot(isDialog()).
532                    check(matches(allOf(
533                            isDisplayed(),
534                            isAssignableFrom(CheckedTextView.class),
535                            isDescendantOfA(isAssignableFrom(ListView.class)),
536                            checkedStateMatcher)));
537        }
538    }
539
540    private void verifyMultiChoiceItemsContent(String[] expectedContent,
541            final boolean[] checkedTracker) {
542        final int expectedCount = expectedContent.length;
543
544        onView(withId(R.id.test_button)).perform(click());
545
546        final ListView listView = mAlertDialog.getListView();
547        assertNotNull("List view is shown", listView);
548
549        final ListAdapter listAdapter = listView.getAdapter();
550        assertEquals("List has " + expectedCount + " entries",
551                expectedCount, listAdapter.getCount());
552        for (int i = 0; i < expectedCount; i++) {
553            assertEquals("List entry #" + i, expectedContent[i], listAdapter.getItem(i));
554        }
555
556        // Test that all items are showing
557        onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
558        verifyMultiChoiceItemsState(expectedContent, checkedTracker);
559
560        // We're going to click item #1 and test that the click listener has been invoked to
561        // update the original state array
562        boolean[] expectedAfterClick1 = checkedTracker.clone();
563        expectedAfterClick1[1] = !expectedAfterClick1[1];
564        onData(allOf(is(instanceOf(String.class)), is(expectedContent[1]))).
565                inRoot(isDialog()).perform(click());
566        verifyMultiChoiceItemsState(expectedContent, expectedAfterClick1);
567
568        // Now click item #1 again and test that the click listener has been invoked to update the
569        // original state array again
570        expectedAfterClick1[1] = !expectedAfterClick1[1];
571        onData(allOf(is(instanceOf(String.class)), is(expectedContent[1]))).
572                inRoot(isDialog()).perform(click());
573        verifyMultiChoiceItemsState(expectedContent, expectedAfterClick1);
574
575        // Now we're going to click the last item and test that the click listener has been invoked
576        // to update the original state array
577        boolean[] expectedAfterClickLast = checkedTracker.clone();
578        expectedAfterClickLast[expectedCount - 1] = !expectedAfterClickLast[expectedCount - 1];
579        onData(allOf(is(instanceOf(String.class)), is(expectedContent[expectedCount - 1]))).
580                inRoot(isDialog()).perform(click());
581        verifyMultiChoiceItemsState(expectedContent, expectedAfterClickLast);
582    }
583
584    @Test
585    @MediumTest
586    public void testMultiChoiceItemsFromRuntimeArray() {
587        final String[] content = new String[] { "Alice", "Bob", "Charlie", "Delta" };
588        final boolean[] checkedTracker = new boolean[] { false, true, false, false };
589        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
590                .setTitle(R.string.alert_dialog_title)
591                .setMultiChoiceItems(
592                        content, checkedTracker,
593                        new DialogInterface.OnMultiChoiceClickListener() {
594                            @Override
595                            public void onClick(DialogInterface dialog, int which,
596                                    boolean isChecked) {
597                                checkedTracker[which] = isChecked;
598                            }
599                        });
600        wireBuilder(builder);
601
602        // Pass the same boolean[] array as used for initialization since our click listener
603        // will be updating its content.
604        verifyMultiChoiceItemsContent(content, checkedTracker);
605    }
606
607    @Test
608    @MediumTest
609    public void testMultiChoiceItemsFromResourcesArray() {
610        final boolean[] checkedTracker = new boolean[] { true, false, true, false };
611        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
612                .setTitle(R.string.alert_dialog_title)
613                .setMultiChoiceItems(R.array.alert_dialog_items, checkedTracker,
614                        new DialogInterface.OnMultiChoiceClickListener() {
615                            @Override
616                            public void onClick(DialogInterface dialog, int which,
617                                    boolean isChecked) {
618                                checkedTracker[which] = isChecked;
619                            }
620                        });
621        wireBuilder(builder);
622
623        verifyMultiChoiceItemsContent(
624                mActivityTestRule.getActivity().getResources().getStringArray(
625                        R.array.alert_dialog_items),
626                checkedTracker);
627    }
628
629    /**
630     * Helper method to verify the state of the single-choice items list. It gets the String
631     * array of content and verifies that:
632     *
633     * 1. The items in the array are rendered as CheckedTextViews inside a ListView
634     * 2. Each item in the array is displayed
635     * 3. Only one row in the ListView is checked, and that corresponds to the passed
636     *    integer index.
637     */
638    private void verifySingleChoiceItemsState(String[] expectedContent,
639            int currentlyExpectedSelectionIndex) {
640        final int expectedCount = expectedContent.length;
641
642        final ListView listView = mAlertDialog.getListView();
643        assertNotNull("List view is shown", listView);
644
645        final ListAdapter listAdapter = listView.getAdapter();
646        assertEquals("List has " + expectedCount + " entries",
647                expectedCount, listAdapter.getCount());
648        for (int i = 0; i < expectedCount; i++) {
649            assertEquals("List entry #" + i, expectedContent[i], listAdapter.getItem(i));
650        }
651
652        for (int i = 0; i < expectedCount; i++) {
653            Matcher checkedStateMatcher = (i == currentlyExpectedSelectionIndex) ?
654                    TestUtilsMatchers.isCheckedTextView() :
655                    TestUtilsMatchers.isNonCheckedTextView();
656            // Check that the corresponding row is rendered as CheckedTextView with expected
657            // checked state.
658            onData(allOf(is(instanceOf(String.class)), is(expectedContent[i]))).inRoot(isDialog()).
659                    check(matches(allOf(
660                            isDisplayed(),
661                            isAssignableFrom(CheckedTextView.class),
662                            isDescendantOfA(isAssignableFrom(ListView.class)),
663                            checkedStateMatcher)));
664        }
665    }
666
667    private void verifySingleChoiceItemsContent(String[] expectedContent,
668            int initialSelectionIndex, DialogInterface.OnClickListener onClickListener) {
669        final int expectedCount = expectedContent.length;
670        int currentlyExpectedSelectionIndex = initialSelectionIndex;
671
672        onView(withId(R.id.test_button)).perform(click());
673
674        // Test that all items are showing
675        onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
676        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
677
678        // We're going to click the first unselected item and test that the click listener has
679        // been invoked.
680        currentlyExpectedSelectionIndex = (currentlyExpectedSelectionIndex == 0) ? 1 : 0;
681        onData(allOf(is(instanceOf(String.class)),
682                is(expectedContent[currentlyExpectedSelectionIndex]))).
683                    inRoot(isDialog()).perform(click());
684        verify(onClickListener, times(1)).onClick(mAlertDialog, currentlyExpectedSelectionIndex);
685        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
686
687        // Now click the same item again and test that the selection has not changed
688        onData(allOf(is(instanceOf(String.class)),
689                is(expectedContent[currentlyExpectedSelectionIndex]))).
690                inRoot(isDialog()).perform(click());
691        verify(onClickListener, times(2)).onClick(mAlertDialog, currentlyExpectedSelectionIndex);
692        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
693
694        // Now we're going to click the last item and test that the click listener has been invoked
695        // to update the original state array
696        currentlyExpectedSelectionIndex = expectedCount - 1;
697        onData(allOf(is(instanceOf(String.class)),
698                is(expectedContent[currentlyExpectedSelectionIndex]))).
699                inRoot(isDialog()).perform(click());
700        verify(onClickListener, times(1)).onClick(mAlertDialog, currentlyExpectedSelectionIndex);
701        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
702    }
703
704    @Test
705    @LargeTest
706    public void testSingleChoiceItemsFromRuntimeArray() {
707        final String[] content = new String[] { "Alice", "Bob", "Charlie", "Delta" };
708        final DialogInterface.OnClickListener mockClickListener =
709                mock(DialogInterface.OnClickListener.class);
710        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
711                .setTitle(R.string.alert_dialog_title)
712                .setSingleChoiceItems(content, 2, mockClickListener);
713        wireBuilder(builder);
714
715        verifySingleChoiceItemsContent(content, 2, mockClickListener);
716    }
717
718    @Test
719    @LargeTest
720    public void testSingleChoiceItemsFromResourcesArray() {
721        final DialogInterface.OnClickListener mockClickListener =
722                mock(DialogInterface.OnClickListener.class);
723        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
724                .setTitle(R.string.alert_dialog_title)
725                .setSingleChoiceItems(R.array.alert_dialog_items, 1, mockClickListener);
726        wireBuilder(builder);
727
728        verifySingleChoiceItemsContent(new String[] { "Albania", "Belize", "Chad", "Djibouti" }, 1,
729                mockClickListener);
730    }
731
732    // Tests for icon logic
733
734    @Test
735    @MediumTest
736    public void testIconResource() {
737        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
738                .setTitle(R.string.alert_dialog_title)
739                .setMessage(R.string.alert_dialog_content)
740                .setIcon(R.drawable.test_drawable_red);
741
742        wireBuilder(builder);
743
744        onView(withId(R.id.test_button)).perform(click());
745
746        // Find the title icon as a visible view that is the sibling of our title
747        ViewInteraction titleIconInteraction = onView(allOf(
748                isAssignableFrom(ImageView.class),
749                isDisplayed(),
750                hasSibling(withText("Dialog title"))));
751        // And check that it's the expected red color
752        titleIconInteraction.check(matches(TestUtilsMatchers.drawable(0xFFFF6030)));
753    }
754
755    @Test
756    @MediumTest
757    public void testIconResourceChangeAfterInitialSetup() throws Throwable {
758        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
759                .setTitle(R.string.alert_dialog_title)
760                .setMessage(R.string.alert_dialog_content)
761                .setIcon(R.drawable.test_drawable_red);
762
763        wireBuilder(builder);
764
765        onView(withId(R.id.test_button)).perform(click());
766
767        // Emulate background loading of the new icon
768        Thread.sleep(1000);
769
770        // Change the icon
771        mActivityTestRule.runOnUiThread(new Runnable() {
772            @Override
773            public void run() {
774                mAlertDialog.setIcon(R.drawable.test_drawable_green);
775            }
776        });
777
778        // Find the title icon as a visible view that is the sibling of our title
779        ViewInteraction titleIconInteraction = onView(allOf(
780                isAssignableFrom(ImageView.class),
781                isDisplayed(),
782                hasSibling(withText("Dialog title"))));
783        // And check that it's the expected (newly set) green color
784        titleIconInteraction.check(matches(TestUtilsMatchers.drawable(0xFF50E080)));
785    }
786
787    @Test
788    @MediumTest
789    public void testIconResourceChangeWithNoInitialSetup() throws Throwable {
790        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
791                .setTitle(R.string.alert_dialog_title)
792                .setMessage(R.string.alert_dialog_content);
793
794        wireBuilder(builder);
795
796        onView(withId(R.id.test_button)).perform(click());
797
798        // Emulate background loading of the new icon
799        Thread.sleep(1000);
800
801        // Change the icon
802        mActivityTestRule.runOnUiThread(new Runnable() {
803            @Override
804            public void run() {
805                mAlertDialog.setIcon(R.drawable.test_drawable_green);
806            }
807        });
808
809        // Find the title icon as a visible view that is the sibling of our title
810        ViewInteraction titleIconInteraction = onView(allOf(
811                isAssignableFrom(ImageView.class),
812                isDisplayed(),
813                hasSibling(withText("Dialog title"))));
814        // And check that it's the expected (newly set) green color
815        titleIconInteraction.check(matches(TestUtilsMatchers.drawable(0xFF50E080)));
816    }
817
818    @Test
819    @MediumTest
820    public void testIconResourceRemoveAfterInitialSetup() throws Throwable {
821        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
822                .setTitle(R.string.alert_dialog_title)
823                .setMessage(R.string.alert_dialog_content)
824                .setIcon(R.drawable.test_drawable_red);
825
826        wireBuilder(builder);
827
828        onView(withId(R.id.test_button)).perform(click());
829
830        // Emulate background resetting of the icon
831        Thread.sleep(1000);
832
833        // Change the icon
834        mActivityTestRule.runOnUiThread(new Runnable() {
835            @Override
836            public void run() {
837                mAlertDialog.setIcon(0);
838            }
839        });
840
841        // Find the title icon as a visible view that is the sibling of our title
842        ViewInteraction titleIconInteraction = onView(allOf(
843                isAssignableFrom(ImageView.class),
844                isDisplayed(),
845                hasSibling(withText("Dialog title"))));
846        // And check that we couldn't find the title icon (since it's expected to be GONE)
847        titleIconInteraction.check(doesNotExist());
848    }
849
850    @Test
851    @MediumTest
852    public void testIconDrawable() {
853        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
854                .setTitle(R.string.alert_dialog_title)
855                .setMessage(R.string.alert_dialog_content)
856                .setIcon(new TestDrawable(0xFF807060, 40, 40));
857
858        wireBuilder(builder);
859
860        onView(withId(R.id.test_button)).perform(click());
861
862        // Find the title icon as a visible view that is the sibling of our title
863        ViewInteraction titleIconInteraction = onView(allOf(
864                isAssignableFrom(ImageView.class),
865                isDisplayed(),
866                hasSibling(withText("Dialog title"))));
867        // And check that it's the expected red color
868        titleIconInteraction.check(matches(TestUtilsMatchers.drawable(0xFF807060)));
869    }
870
871    @Test
872    @MediumTest
873    public void testIconResourceDrawableAfterInitialSetup() throws Throwable {
874        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
875                .setTitle(R.string.alert_dialog_title)
876                .setMessage(R.string.alert_dialog_content)
877                .setIcon(new TestDrawable(0xFF807060, 40, 40));
878
879        wireBuilder(builder);
880
881        onView(withId(R.id.test_button)).perform(click());
882
883        // Emulate background loading of the new icon
884        Thread.sleep(1000);
885
886        // Change the icon
887        mActivityTestRule.runOnUiThread(new Runnable() {
888            @Override
889            public void run() {
890                mAlertDialog.setIcon(new TestDrawable(0xFF503090, 40, 40));
891            }
892        });
893
894        // Find the title icon as a visible view that is the sibling of our title
895        ViewInteraction titleIconInteraction = onView(allOf(
896                isAssignableFrom(ImageView.class),
897                isDisplayed(),
898                hasSibling(withText("Dialog title"))));
899        // And check that it's the expected (newly set) green color
900        titleIconInteraction.check(matches(TestUtilsMatchers.drawable(0xFF503090)));
901    }
902
903    @Test
904    @MediumTest
905    public void testIconDrawableChangeWithNoInitialSetup() throws Throwable {
906        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
907                .setTitle(R.string.alert_dialog_title)
908                .setMessage(R.string.alert_dialog_content);
909
910        wireBuilder(builder);
911
912        onView(withId(R.id.test_button)).perform(click());
913
914        // Emulate background loading of the new icon
915        Thread.sleep(1000);
916
917        // Change the icon
918        mActivityTestRule.runOnUiThread(new Runnable() {
919            @Override
920            public void run() {
921                mAlertDialog.setIcon(new TestDrawable(0xFF503090, 40, 40));
922            }
923        });
924
925        // Find the title icon as a visible view that is the sibling of our title
926        ViewInteraction titleIconInteraction = onView(allOf(
927                isAssignableFrom(ImageView.class),
928                isDisplayed(),
929                hasSibling(withText("Dialog title"))));
930        // And check that it's the expected (newly set) green color
931        titleIconInteraction.check(matches(TestUtilsMatchers.drawable(0xFF503090)));
932    }
933
934    @Test
935    @MediumTest
936    public void testIconDrawableRemoveAfterInitialSetup() throws Throwable {
937        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
938                .setTitle(R.string.alert_dialog_title)
939                .setMessage(R.string.alert_dialog_content)
940                .setIcon(new TestDrawable(0xFF807060, 40, 40));
941
942        wireBuilder(builder);
943
944        onView(withId(R.id.test_button)).perform(click());
945
946        // Emulate background resetting of the icon
947        Thread.sleep(1000);
948
949        // Change the icon
950        mActivityTestRule.runOnUiThread(new Runnable() {
951            @Override
952            public void run() {
953                mAlertDialog.setIcon(null);
954            }
955        });
956
957        // Find the title icon as a visible view that is the sibling of our title
958        ViewInteraction titleIconInteraction = onView(allOf(
959                isAssignableFrom(ImageView.class),
960                isDisplayed(),
961                hasSibling(withText("Dialog title"))));
962        // And check that we couldn't find the title icon (since it's expected to be GONE)
963        titleIconInteraction.check(doesNotExist());
964    }
965
966    // Tests for buttons logic
967
968    /**
969     * Helper method to verify visibility and text content of dialog buttons. Gets expected texts
970     * for three buttons (positive, negative and neutral) and for each button verifies that:
971     *
972     * If the text is null or empty, that the button is GONE
973     * If the text is not empty, that the button is VISIBLE and shows the corresponding text
974     */
975    private void verifyButtonContent(String expectedPositiveButtonText,
976            String expectedNegativeButtonText, String expectedNeutralButtonText) {
977        assertTrue("Dialog is showing", mAlertDialog.isShowing());
978
979        final Button positiveButton = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
980        final Button negativeButton = mAlertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
981        final Button neutralButton = mAlertDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
982
983        if (TextUtils.isEmpty(expectedPositiveButtonText)) {
984            assertEquals("Positive button not shown", View.GONE, positiveButton.getVisibility());
985        } else {
986            assertEquals("Positive button shown", View.VISIBLE, positiveButton.getVisibility());
987            assertEquals("Positive button text", expectedPositiveButtonText,
988                    positiveButton.getText());
989        }
990
991        if (TextUtils.isEmpty(expectedNegativeButtonText)) {
992            assertEquals("Negative button not shown", View.GONE, negativeButton.getVisibility());
993        } else {
994            assertEquals("Negative button shown", View.VISIBLE, negativeButton.getVisibility());
995            assertEquals("Negative button text", expectedNegativeButtonText,
996                    negativeButton.getText());
997        }
998
999        if (TextUtils.isEmpty(expectedNeutralButtonText)) {
1000            assertEquals("Neutral button not shown", View.GONE, neutralButton.getVisibility());
1001        } else {
1002            assertEquals("Neutral button shown", View.VISIBLE, neutralButton.getVisibility());
1003            assertEquals("Neutral button text", expectedNeutralButtonText,
1004                    neutralButton.getText());
1005        }
1006    }
1007
1008    /**
1009     * Helper method to verify dialog state after a button has been clicked.
1010     */
1011    private void verifyPostButtonClickState(int whichButtonClicked,
1012            DialogInterface.OnDismissListener onDismissListener,
1013            Handler messageHandler) {
1014        // Verify that a Message with expected 'what' field has been posted on our mock handler
1015        ArgumentCaptor<Message> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class);
1016        verify(messageHandler, times(1)).sendMessageDelayed(
1017                messageArgumentCaptor.capture(), anyLong());
1018        assertEquals("Button clicked", whichButtonClicked, messageArgumentCaptor.getValue().what);
1019        // Verify that the dialog is no longer showing
1020        assertFalse("Dialog is not showing", mAlertDialog.isShowing());
1021        if (onDismissListener != null) {
1022            // And that our mock listener has been called when the dialog was dismissed
1023            verify(onDismissListener, times(1)).onDismiss(mAlertDialog);
1024        }
1025    }
1026
1027    /**
1028     * Helper method to verify dialog state after a button has been clicked.
1029     */
1030    private void verifyPostButtonClickState(int whichButtonClicked,
1031            DialogInterface.OnClickListener onClickListener,
1032            DialogInterface.OnDismissListener onDismissListener) {
1033        if (onClickListener != null) {
1034            verify(onClickListener, times(1)).onClick(mAlertDialog, whichButtonClicked);
1035        }
1036        assertFalse("Dialog is not showing", mAlertDialog.isShowing());
1037        if (onDismissListener != null) {
1038            verify(onDismissListener, times(1)).onDismiss(mAlertDialog);
1039        }
1040    }
1041
1042    /**
1043     * Helper method to verify button-related logic for setXXXButton on AlertDialog.Builder
1044     * that gets CharSequence parameter. This method configures the dialog buttons based
1045     * on the passed texts (some of which may be null or empty, in which case the corresponding
1046     * button is not configured), tests the buttons visibility and texts, simulates a click
1047     * on the specified button and then tests the post-click dialog state.
1048     */
1049    private void verifyDialogButtons(String positiveButtonText, String negativeButtonText,
1050            String neutralButtonText, int whichButtonToClick) {
1051        final AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
1052                .setTitle(R.string.alert_dialog_title);
1053        // Configure buttons with non-empty texts
1054        DialogInterface.OnClickListener mockClickListener =
1055                mock(DialogInterface.OnClickListener.class);
1056        if (!TextUtils.isEmpty(positiveButtonText)) {
1057            builder.setPositiveButton(positiveButtonText, mockClickListener);
1058        }
1059        if (!TextUtils.isEmpty(negativeButtonText)) {
1060            builder.setNegativeButton(negativeButtonText, mockClickListener);
1061        }
1062        if (!TextUtils.isEmpty(neutralButtonText)) {
1063            builder.setNeutralButton(neutralButtonText, mockClickListener);
1064        }
1065        // Set a dismiss listener to verify that the dialog is dismissed on clicking any button
1066        DialogInterface.OnDismissListener mockDismissListener =
1067                mock(DialogInterface.OnDismissListener.class);
1068        builder.setOnDismissListener(mockDismissListener);
1069
1070        // Wire the builder to the button click and click that button to show the dialog
1071        wireBuilder(builder);
1072        onView(withId(R.id.test_button)).perform(click());
1073
1074        // Check that the dialog is showing the configured buttons
1075        verifyButtonContent(positiveButtonText, negativeButtonText, neutralButtonText);
1076
1077        // Click the specified button and verify the post-click state
1078        String textOfButtonToClick = null;
1079        switch (whichButtonToClick) {
1080            case DialogInterface.BUTTON_POSITIVE:
1081                textOfButtonToClick = positiveButtonText;
1082                break;
1083            case DialogInterface.BUTTON_NEGATIVE:
1084                textOfButtonToClick = negativeButtonText;
1085                break;
1086            case DialogInterface.BUTTON_NEUTRAL:
1087                textOfButtonToClick = neutralButtonText;
1088                break;
1089        }
1090        onView(withText(textOfButtonToClick)).inRoot(isDialog()).perform(click());
1091        verifyPostButtonClickState(whichButtonToClick, mockClickListener, mockDismissListener);
1092    }
1093
1094    /**
1095     * Helper method to verify button-related logic for setXXXButton on AlertDialog.Builder
1096     * that gets string resource ID parameter. This method configures the dialog buttons based
1097     * on the passed texts (some of which may be null or empty, in which case the corresponding
1098     * button is not configured), tests the buttons visibility and texts, simulates a click
1099     * on the specified button and then tests the post-click dialog state.
1100     */
1101    private void verifyDialogButtons(@StringRes int positiveButtonTextResId,
1102            @StringRes int negativeButtonTextResId,
1103            @StringRes int neutralButtonTextResId, int whichButtonToClick) {
1104        Context context = mActivityTestRule.getActivity();
1105        String positiveButtonText = null;
1106        String negativeButtonText = null;
1107        String neutralButtonText = null;
1108
1109        final AlertDialog.Builder builder = new AlertDialog.Builder(context)
1110                .setTitle(R.string.alert_dialog_title);
1111        DialogInterface.OnClickListener mockClickListener =
1112                mock(DialogInterface.OnClickListener.class);
1113        // Configure buttons with non-zero text resource IDs
1114        if (positiveButtonTextResId != 0) {
1115            positiveButtonText = context.getString(positiveButtonTextResId);
1116            builder.setPositiveButton(positiveButtonTextResId, mockClickListener);
1117        }
1118        if (negativeButtonTextResId != 0) {
1119            negativeButtonText = context.getString(negativeButtonTextResId);
1120            builder.setNegativeButton(negativeButtonTextResId, mockClickListener);
1121        }
1122        if (neutralButtonTextResId != 0) {
1123            neutralButtonText = context.getString(neutralButtonTextResId);
1124            builder.setNeutralButton(neutralButtonTextResId, mockClickListener);
1125        }
1126        // Set a dismiss listener to verify that the dialog is dismissed on clicking any button
1127        DialogInterface.OnDismissListener mockDismissListener =
1128                mock(DialogInterface.OnDismissListener.class);
1129        builder.setOnDismissListener(mockDismissListener);
1130
1131        // Wire the builder to the button click and click that button to show the dialog
1132        wireBuilder(builder);
1133        onView(withId(R.id.test_button)).perform(click());
1134
1135        // Check that the dialog is showing the configured buttons
1136        verifyButtonContent(positiveButtonText, negativeButtonText, neutralButtonText);
1137
1138        // Click the specified button and verify the post-click state
1139        String textOfButtonToClick = null;
1140        switch (whichButtonToClick) {
1141            case DialogInterface.BUTTON_POSITIVE:
1142                textOfButtonToClick = positiveButtonText;
1143                break;
1144            case DialogInterface.BUTTON_NEGATIVE:
1145                textOfButtonToClick = negativeButtonText;
1146                break;
1147            case DialogInterface.BUTTON_NEUTRAL:
1148                textOfButtonToClick = neutralButtonText;
1149                break;
1150        }
1151        onView(withText(textOfButtonToClick)).inRoot(isDialog()).perform(click());
1152        verifyPostButtonClickState(whichButtonToClick, mockClickListener, mockDismissListener);
1153    }
1154
1155    /**
1156     * Helper method to verify button-related logic for setButton on AlertDialog after the
1157     * dialog has been create()'d. This method configures the dialog buttons based
1158     * on the passed texts (some of which may be null or empty, in which case the corresponding
1159     * button is not configured), tests the buttons visibility and texts, simulates a click
1160     * on the specified button and then tests the post-click dialog state.
1161     */
1162    private void verifyDialogButtonsPostCreation(final String positiveButtonText,
1163            final String negativeButtonText, final String neutralButtonText,
1164            int whichButtonToClick) {
1165        final AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
1166                .setTitle(R.string.alert_dialog_title);
1167        // Set a dismiss listener to verify that the dialog is dismissed on clicking any button
1168        DialogInterface.OnDismissListener mockDismissListener =
1169                mock(DialogInterface.OnDismissListener.class);
1170        builder.setOnDismissListener(mockDismissListener);
1171
1172        final DialogInterface.OnClickListener mockClickListener =
1173                mock(DialogInterface.OnClickListener.class);
1174
1175        mButton.setOnClickListener(new View.OnClickListener() {
1176            @Override
1177            public void onClick(View v) {
1178                mAlertDialog = builder.create();
1179                // Configure buttons with non-empty texts
1180                if (!TextUtils.isEmpty(positiveButtonText)) {
1181                    mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, positiveButtonText,
1182                            mockClickListener);
1183                }
1184                if (!TextUtils.isEmpty(negativeButtonText)) {
1185                    mAlertDialog.setButton(DialogInterface.BUTTON_NEGATIVE, negativeButtonText,
1186                            mockClickListener);
1187                }
1188                if (!TextUtils.isEmpty(neutralButtonText)) {
1189                    mAlertDialog.setButton(DialogInterface.BUTTON_NEUTRAL, neutralButtonText,
1190                            mockClickListener);
1191                }
1192
1193                mAlertDialog.show();
1194            }
1195        });
1196
1197        // Click the button to create the dialog, configure the buttons and show the dialog
1198        onView(withId(R.id.test_button)).perform(click());
1199
1200        // Check that the dialog is showing the configured buttons
1201        verifyButtonContent(positiveButtonText, negativeButtonText, neutralButtonText);
1202
1203        // Click the specified button and verify the post-click state
1204        String textOfButtonToClick = null;
1205        switch (whichButtonToClick) {
1206            case DialogInterface.BUTTON_POSITIVE:
1207                textOfButtonToClick = positiveButtonText;
1208                break;
1209            case DialogInterface.BUTTON_NEGATIVE:
1210                textOfButtonToClick = negativeButtonText;
1211                break;
1212            case DialogInterface.BUTTON_NEUTRAL:
1213                textOfButtonToClick = neutralButtonText;
1214                break;
1215        }
1216        onView(withText(textOfButtonToClick)).inRoot(isDialog()).perform(click());
1217        verifyPostButtonClickState(whichButtonToClick, mockClickListener, null);
1218    }
1219
1220    /**
1221     * Helper method to verify button-related logic for setButton on AlertDialog after the
1222     * dialog has been create()'d. This method configures the dialog buttons based
1223     * on the passed texts (some of which may be null or empty, in which case the corresponding
1224     * button is not configured), tests the buttons visibility and texts, simulates a click
1225     * on the specified button and then tests the post-click dialog state.
1226     */
1227    private void verifyDialogButtonsPostCreationMessage(final String positiveButtonText,
1228            final String negativeButtonText, final String neutralButtonText,
1229            int whichButtonToClick) {
1230        final AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
1231                .setTitle(R.string.alert_dialog_title);
1232        // Set a dismiss listener to verify that the dialog is dismissed on clicking any button
1233        DialogInterface.OnDismissListener mockDismissListener =
1234                mock(DialogInterface.OnDismissListener.class);
1235        builder.setOnDismissListener(mockDismissListener);
1236
1237        final Handler mockMessageHandler = mock(Handler.class);
1238        mButton.setOnClickListener(new View.OnClickListener() {
1239            @Override
1240            public void onClick(View v) {
1241                mAlertDialog = builder.create();
1242                // Configure buttons with non-empty texts
1243                if (!TextUtils.isEmpty(positiveButtonText)) {
1244                    mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, positiveButtonText,
1245                            Message.obtain(mockMessageHandler, DialogInterface.BUTTON_POSITIVE));
1246                }
1247                if (!TextUtils.isEmpty(negativeButtonText)) {
1248                    mAlertDialog.setButton(DialogInterface.BUTTON_NEGATIVE, negativeButtonText,
1249                            Message.obtain(mockMessageHandler, DialogInterface.BUTTON_NEGATIVE));
1250                }
1251                if (!TextUtils.isEmpty(neutralButtonText)) {
1252                    mAlertDialog.setButton(DialogInterface.BUTTON_NEUTRAL, neutralButtonText,
1253                            Message.obtain(mockMessageHandler, DialogInterface.BUTTON_NEUTRAL));
1254                }
1255
1256                mAlertDialog.show();
1257            }
1258        });
1259
1260        // Click the button to create the dialog, configure the buttons and show the dialog
1261        onView(withId(R.id.test_button)).perform(click());
1262
1263        // Check that the dialog is showing the configured buttons
1264        verifyButtonContent(positiveButtonText, negativeButtonText, neutralButtonText);
1265
1266        // Click the specified button and verify the post-click state
1267        String textOfButtonToClick = null;
1268        switch (whichButtonToClick) {
1269            case DialogInterface.BUTTON_POSITIVE:
1270                textOfButtonToClick = positiveButtonText;
1271                break;
1272            case DialogInterface.BUTTON_NEGATIVE:
1273                textOfButtonToClick = negativeButtonText;
1274                break;
1275            case DialogInterface.BUTTON_NEUTRAL:
1276                textOfButtonToClick = neutralButtonText;
1277                break;
1278        }
1279        onView(withText(textOfButtonToClick)).inRoot(isDialog()).perform(click());
1280        verifyPostButtonClickState(whichButtonToClick, mockDismissListener, mockMessageHandler);
1281    }
1282
1283    @Test
1284    @MediumTest
1285    public void testButtonVisibility() {
1286        final String positiveButtonText = "Positive button";
1287        final String negativeButtonText = "Negative button";
1288        final String neutralButtonText = "Neutral button";
1289        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
1290                .setTitle(R.string.alert_dialog_title)
1291                .setPositiveButton(positiveButtonText, null)
1292                .setNegativeButton(negativeButtonText, null)
1293                .setNeutralButton(neutralButtonText, null);
1294        wireBuilder(builder);
1295
1296        onView(withId(R.id.test_button)).perform(click());
1297
1298        // Positive button should be fully displayed with no text eliding
1299        onView(withText(positiveButtonText)).inRoot(isDialog()).check(
1300                matches(isCompletelyDisplayed()));
1301        onView(withText(positiveButtonText)).inRoot(isDialog()).check(
1302                matches(not(hasEllipsizedText())));
1303
1304        // Negative button should be fully displayed with no text eliding
1305        onView(withText(negativeButtonText)).inRoot(isDialog()).check(
1306                matches(isCompletelyDisplayed()));
1307        onView(withText(negativeButtonText)).inRoot(isDialog()).check(
1308                matches(not(hasEllipsizedText())));
1309
1310        // Neutral button should be fully displayed with no text eliding
1311        onView(withText(neutralButtonText)).inRoot(isDialog()).check(
1312                matches(isCompletelyDisplayed()));
1313        onView(withText(neutralButtonText)).inRoot(isDialog()).check(
1314                matches(not(hasEllipsizedText())));
1315    }
1316
1317    @Test
1318    @LargeTest
1319    public void testButtons() {
1320        // Positive-only button
1321        verifyDialogButtons("Positive", null, null, AlertDialog.BUTTON_POSITIVE);
1322        verifyDialogButtons(R.string.alert_dialog_positive_button, 0, 0,
1323                AlertDialog.BUTTON_POSITIVE);
1324        verifyDialogButtonsPostCreation("Post positive", null, null, AlertDialog.BUTTON_POSITIVE);
1325        verifyDialogButtonsPostCreationMessage("Message positive", null, null,
1326                AlertDialog.BUTTON_POSITIVE);
1327
1328        // Negative-only button
1329        verifyDialogButtons(null, "Negative", null, AlertDialog.BUTTON_NEGATIVE);
1330        verifyDialogButtons(0, R.string.alert_dialog_negative_button, 0,
1331                AlertDialog.BUTTON_NEGATIVE);
1332        verifyDialogButtonsPostCreation(null, "Post negative", null, AlertDialog.BUTTON_NEGATIVE);
1333        verifyDialogButtonsPostCreationMessage(null, "Message negative", null,
1334                AlertDialog.BUTTON_NEGATIVE);
1335
1336        // Neutral-only button
1337        verifyDialogButtons(null, null, "Neutral", AlertDialog.BUTTON_NEUTRAL);
1338        verifyDialogButtons(0, 0, R.string.alert_dialog_neutral_button, AlertDialog.BUTTON_NEUTRAL);
1339        verifyDialogButtonsPostCreation(null, null, "Post neutral", AlertDialog.BUTTON_NEUTRAL);
1340        verifyDialogButtonsPostCreationMessage(null, null, "Message neutral",
1341                AlertDialog.BUTTON_NEUTRAL);
1342
1343        // Show positive and negative, click positive
1344        verifyDialogButtons(R.string.alert_dialog_positive_button,
1345                R.string.alert_dialog_negative_button, 0, AlertDialog.BUTTON_POSITIVE);
1346
1347        // Show positive and neutral, click neutral
1348        verifyDialogButtons("Positive", null, "Neutral", AlertDialog.BUTTON_NEUTRAL);
1349
1350        // Show negative and neutral, click negative
1351        verifyDialogButtonsPostCreationMessage(null, "Message negative",
1352                "Message neutral", AlertDialog.BUTTON_NEGATIVE);
1353
1354        // Show all, click positive
1355        verifyDialogButtonsPostCreation("Post positive", "Post negative", "Post neutral",
1356                AlertDialog.BUTTON_POSITIVE);
1357    }
1358
1359    private static class TestDrawable extends ColorDrawable {
1360        private int mWidth;
1361        private int mHeight;
1362
1363        public TestDrawable(@ColorInt int color, int width, int height) {
1364            super(color);
1365            mWidth = width;
1366            mHeight = height;
1367        }
1368
1369        @Override
1370        public int getIntrinsicWidth() {
1371            return mWidth;
1372        }
1373
1374        @Override
1375        public int getIntrinsicHeight() {
1376            return mHeight;
1377        }
1378    }
1379}
1380