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 androidx.appcompat.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.ViewAssertions.matches;
22import static android.support.test.espresso.matcher.RootMatchers.isDialog;
23import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
24import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
25import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
26import static android.support.test.espresso.matcher.ViewMatchers.withId;
27import static android.support.test.espresso.matcher.ViewMatchers.withText;
28
29import static org.hamcrest.Matchers.instanceOf;
30import static org.hamcrest.core.AllOf.allOf;
31import static org.hamcrest.core.Is.is;
32import static org.junit.Assert.assertEquals;
33import static org.junit.Assert.assertNotNull;
34import static org.mockito.Matchers.any;
35import static org.mockito.Mockito.mock;
36import static org.mockito.Mockito.never;
37import static org.mockito.Mockito.times;
38import static org.mockito.Mockito.verify;
39
40import android.content.ContentValues;
41import android.content.Context;
42import android.content.DialogInterface;
43import android.database.Cursor;
44import android.database.sqlite.SQLiteCursor;
45import android.database.sqlite.SQLiteDatabase;
46import android.support.test.espresso.DataInteraction;
47import android.support.test.filters.LargeTest;
48import android.support.test.filters.MediumTest;
49import android.support.test.rule.ActivityTestRule;
50import android.support.test.runner.AndroidJUnit4;
51import android.view.View;
52import android.widget.Button;
53import android.widget.CheckedTextView;
54import android.widget.ListAdapter;
55import android.widget.ListView;
56
57import androidx.appcompat.test.R;
58import androidx.appcompat.testutils.TestUtilsMatchers;
59
60import org.hamcrest.Matcher;
61import org.junit.After;
62import org.junit.Before;
63import org.junit.Rule;
64import org.junit.Test;
65import org.junit.runner.RunWith;
66
67import java.io.File;
68
69@MediumTest
70@RunWith(AndroidJUnit4.class)
71public class AlertDialogCursorTest {
72    @Rule
73    public final ActivityTestRule<AlertDialogTestActivity> mActivityTestRule;
74
75    private Button mButton;
76
77    private static final String TEXT_COLUMN_NAME = "text";
78    private static final String CHECKED_COLUMN_NAME = "checked";
79
80    private String[] mTextContent;
81    private boolean[] mCheckedContent;
82
83    private String[] mProjectionWithChecked;
84    private String[] mProjectionWithoutChecked;
85
86    private SQLiteDatabase mDatabase;
87    private File mDatabaseFile;
88    private Cursor mCursor;
89
90    private AlertDialog mAlertDialog;
91
92    public AlertDialogCursorTest() {
93        mActivityTestRule = new ActivityTestRule<>(AlertDialogTestActivity.class);
94    }
95
96    @Before
97    public void setUp() {
98        // Ideally these constant arrays would be defined as final static fields on the
99        // class level, but for some reason those get reset to null on v9- devices after
100        // the first test method has been executed.
101        mTextContent = new String[] { "Adele", "Beyonce", "Ciara", "Dido" };
102        mCheckedContent = new boolean[] { false, false, true, false };
103
104        mProjectionWithChecked = new String[] {
105                "_id",                       // 0
106                TEXT_COLUMN_NAME,            // 1
107                CHECKED_COLUMN_NAME          // 2
108        };
109        mProjectionWithoutChecked = new String[] {
110                "_id",                       // 0
111                TEXT_COLUMN_NAME             // 1
112        };
113
114        final AlertDialogTestActivity activity = mActivityTestRule.getActivity();
115        mButton = (Button) activity.findViewById(R.id.test_button);
116
117        File dbDir = activity.getDir("tests", Context.MODE_PRIVATE);
118        mDatabaseFile = new File(dbDir, "database_alert_dialog_test.db");
119        if (mDatabaseFile.exists()) {
120            mDatabaseFile.delete();
121        }
122        mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null);
123        assertNotNull(mDatabase);
124        // Create and populate a test table
125        mDatabase.execSQL(
126                "CREATE TABLE test (_id INTEGER PRIMARY KEY, " + TEXT_COLUMN_NAME +
127                        " TEXT, " + CHECKED_COLUMN_NAME + " INTEGER);");
128        for (int i = 0; i < mTextContent.length; i++) {
129            mDatabase.execSQL("INSERT INTO test (" + TEXT_COLUMN_NAME + ", " +
130                    CHECKED_COLUMN_NAME + ") VALUES ('" + mTextContent[i] + "', " +
131                    (mCheckedContent[i] ? "1" : "0") + ");");
132        }
133    }
134
135    @After
136    public void tearDown() throws Throwable {
137        if (mCursor != null) {
138            // Close the cursor on the UI thread as the list view in the alert dialog
139            // will get notified of any change to the underlying cursor.
140            mActivityTestRule.runOnUiThread(new Runnable() {
141                @Override
142                public void run() {
143                    mCursor.close();
144                    mCursor = null;
145                }
146            });
147        }
148        if (mDatabase != null) {
149            mDatabase.close();
150        }
151        if (mDatabaseFile != null) {
152            mDatabaseFile.delete();
153        }
154        if (mAlertDialog != null) {
155            mAlertDialog.dismiss();
156        }
157    }
158
159    private void wireBuilder(final AlertDialog.Builder builder) {
160        mButton.setOnClickListener(new View.OnClickListener() {
161            @Override
162            public void onClick(View v) {
163                mAlertDialog = builder.show();
164            }
165        });
166    }
167
168    private void verifySimpleItemsContent(String[] expectedContent,
169            DialogInterface.OnClickListener onClickListener) {
170        final int expectedCount = expectedContent.length;
171
172        onView(withId(R.id.test_button)).perform(click());
173
174        final ListView listView = mAlertDialog.getListView();
175        assertNotNull("List view is shown", listView);
176
177        final ListAdapter listAdapter = listView.getAdapter();
178        assertEquals("List has " + expectedCount + " entries",
179                expectedCount, listAdapter.getCount());
180
181        // Test that all items are showing
182        onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
183        for (int i = 0; i < expectedCount; i++) {
184            DataInteraction rowInteraction = onData(allOf(
185                    is(instanceOf(SQLiteCursor.class)),
186                    TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[i])));
187            rowInteraction.inRoot(isDialog()).check(matches(isDisplayed()));
188        }
189
190        // Verify that our click listener hasn't been called yet
191        verify(onClickListener, never()).onClick(any(DialogInterface.class), any(int.class));
192        // Test that a click on an item invokes the registered listener
193        int indexToClick = expectedCount - 2;
194        DataInteraction interactionForClick = onData(allOf(
195                is(instanceOf(SQLiteCursor.class)),
196                TestUtilsMatchers.withCursorItemContent(
197                        TEXT_COLUMN_NAME, expectedContent[indexToClick])));
198        interactionForClick.inRoot(isDialog()).perform(click());
199        verify(onClickListener, times(1)).onClick(mAlertDialog, indexToClick);
200    }
201
202    @Test
203    public void testSimpleItemsFromCursor() {
204        mCursor = mDatabase.query("test", mProjectionWithoutChecked,
205                null, null, null, null, null);
206        assertNotNull(mCursor);
207
208        final DialogInterface.OnClickListener mockClickListener =
209                mock(DialogInterface.OnClickListener.class);
210        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
211                .setTitle(R.string.alert_dialog_title)
212                .setCursor(mCursor, mockClickListener, "text");
213        wireBuilder(builder);
214
215        verifySimpleItemsContent(mTextContent, mockClickListener);
216    }
217
218    /**
219     * Helper method to verify the state of the multi-choice items list. It gets the String
220     * array of content and verifies that:
221     *
222     * 1. The items in the array are rendered as CheckedTextViews inside a ListView
223     * 2. Each item in the array is displayed
224     * 3. Checked state of each row in the ListView corresponds to the matching entry in the
225     *    passed boolean array
226     */
227    private void verifyMultiChoiceItemsState(String[] expectedContent,
228            boolean[] checkedTracker) {
229        final int expectedCount = expectedContent.length;
230
231        final ListView listView = mAlertDialog.getListView();
232        assertNotNull("List view is shown", listView);
233
234        final ListAdapter listAdapter = listView.getAdapter();
235        assertEquals("List has " + expectedCount + " entries",
236                expectedCount, listAdapter.getCount());
237
238        for (int i = 0; i < expectedCount; i++) {
239            Matcher checkedStateMatcher = checkedTracker[i] ? TestUtilsMatchers.isCheckedTextView() :
240                    TestUtilsMatchers.isNonCheckedTextView();
241            // Check that the corresponding row is rendered as CheckedTextView with expected
242            // checked state.
243            DataInteraction rowInteraction = onData(allOf(
244                    is(instanceOf(SQLiteCursor.class)),
245                    TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[i])));
246            rowInteraction.inRoot(isDialog()).
247                    check(matches(allOf(
248                            isDisplayed(),
249                            isAssignableFrom(CheckedTextView.class),
250                            isDescendantOfA(isAssignableFrom(ListView.class)),
251                            checkedStateMatcher)));
252        }
253    }
254
255    private void verifyMultiChoiceItemsContent(String[] expectedContent,
256            final boolean[] checkedTracker) {
257        final int expectedCount = expectedContent.length;
258
259        onView(withId(R.id.test_button)).perform(click());
260
261        final ListView listView = mAlertDialog.getListView();
262        assertNotNull("List view is shown", listView);
263
264        final ListAdapter listAdapter = listView.getAdapter();
265        assertEquals("List has " + expectedCount + " entries",
266                expectedCount, listAdapter.getCount());
267
268        // Test that all items are showing
269        onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
270        verifyMultiChoiceItemsState(expectedContent, checkedTracker);
271
272        // We're going to click item #1 and test that the click listener has been invoked to
273        // update the original state array
274        boolean[] expectedAfterClick1 = checkedTracker.clone();
275        expectedAfterClick1[1] = !expectedAfterClick1[1];
276        DataInteraction interactionForClick = onData(allOf(
277                is(instanceOf(SQLiteCursor.class)),
278                TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[1])));
279        interactionForClick.inRoot(isDialog()).perform(click());
280        verifyMultiChoiceItemsState(expectedContent, expectedAfterClick1);
281
282        // Now click item #1 again and test that the click listener has been invoked to update the
283        // original state array again
284        expectedAfterClick1[1] = !expectedAfterClick1[1];
285        interactionForClick.inRoot(isDialog()).perform(click());
286        verifyMultiChoiceItemsState(expectedContent, expectedAfterClick1);
287
288        // Now we're going to click the last item and test that the click listener has been invoked
289        // to update the original state array
290        boolean[] expectedAfterClickLast = checkedTracker.clone();
291        expectedAfterClickLast[expectedCount - 1] = !expectedAfterClickLast[expectedCount - 1];
292        interactionForClick = onData(allOf(
293                is(instanceOf(SQLiteCursor.class)),
294                TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME,
295                        expectedContent[expectedCount - 1])));
296        interactionForClick.inRoot(isDialog()).perform(click());
297        verifyMultiChoiceItemsState(expectedContent, expectedAfterClickLast);
298    }
299
300    @LargeTest
301    @Test
302    public void testMultiChoiceItemsFromCursor() {
303        mCursor = mDatabase.query("test", mProjectionWithChecked,
304                null, null, null, null, null);
305        assertNotNull(mCursor);
306
307        final boolean[] checkedTracker = mCheckedContent.clone();
308        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
309                .setTitle(R.string.alert_dialog_title)
310                .setMultiChoiceItems(mCursor, CHECKED_COLUMN_NAME, TEXT_COLUMN_NAME,
311                        new DialogInterface.OnMultiChoiceClickListener() {
312                            @Override
313                            public void onClick(DialogInterface dialog, int which,
314                                    boolean isChecked) {
315                                // Update the underlying database with the new checked
316                                // state for the specific row
317                                mCursor.moveToPosition(which);
318                                ContentValues valuesToUpdate = new ContentValues();
319                                valuesToUpdate.put(CHECKED_COLUMN_NAME, isChecked ? 1 : 0);
320                                mDatabase.update("test", valuesToUpdate,
321                                        TEXT_COLUMN_NAME + " = ?",
322                                        new String[] { mCursor.getString(1) } );
323                                mCursor.requery();
324                                checkedTracker[which] = isChecked;
325                            }
326                        });
327        wireBuilder(builder);
328
329        // Pass the same boolean[] array as used for initialization since our click listener
330        // will be updating its content.
331        verifyMultiChoiceItemsContent(mTextContent, checkedTracker);
332    }
333
334    /**
335     * Helper method to verify the state of the single-choice items list. It gets the String
336     * array of content and verifies that:
337     *
338     * 1. The items in the array are rendered as CheckedTextViews inside a ListView
339     * 2. Each item in the array is displayed
340     * 3. Only one row in the ListView is checked, and that corresponds to the passed
341     *    integer index.
342     */
343    private void verifySingleChoiceItemsState(String[] expectedContent,
344            int currentlyExpectedSelectionIndex) {
345        final int expectedCount = expectedContent.length;
346
347        final ListView listView = mAlertDialog.getListView();
348        assertNotNull("List view is shown", listView);
349
350        final ListAdapter listAdapter = listView.getAdapter();
351        assertEquals("List has " + expectedCount + " entries",
352                expectedCount, listAdapter.getCount());
353
354        for (int i = 0; i < expectedCount; i++) {
355            Matcher checkedStateMatcher = (i == currentlyExpectedSelectionIndex) ?
356                    TestUtilsMatchers.isCheckedTextView() :
357                    TestUtilsMatchers.isNonCheckedTextView();
358            // Check that the corresponding row is rendered as CheckedTextView with expected
359            // checked state.
360            DataInteraction rowInteraction = onData(allOf(
361                    is(instanceOf(SQLiteCursor.class)),
362                    TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[i])));
363            rowInteraction.inRoot(isDialog()).
364                    check(matches(allOf(
365                            isDisplayed(),
366                            isAssignableFrom(CheckedTextView.class),
367                            isDescendantOfA(isAssignableFrom(ListView.class)),
368                            checkedStateMatcher)));
369        }
370    }
371
372    private void verifySingleChoiceItemsContent(String[] expectedContent,
373            int initialSelectionIndex, DialogInterface.OnClickListener onClickListener) {
374        final int expectedCount = expectedContent.length;
375        int currentlyExpectedSelectionIndex = initialSelectionIndex;
376
377        onView(withId(R.id.test_button)).perform(click());
378
379        // Test that all items are showing
380        onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
381        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
382
383        // We're going to click the first unselected item and test that the click listener has
384        // been invoked.
385        currentlyExpectedSelectionIndex = (currentlyExpectedSelectionIndex == 0) ? 1 : 0;
386        DataInteraction interactionForClick = onData(allOf(
387                is(instanceOf(SQLiteCursor.class)),
388                TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME,
389                        expectedContent[currentlyExpectedSelectionIndex])));
390        interactionForClick.inRoot(isDialog()).perform(click());
391        verify(onClickListener, times(1)).onClick(mAlertDialog, currentlyExpectedSelectionIndex);
392        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
393
394        // Now click the same item again and test that the selection has not changed
395        interactionForClick.inRoot(isDialog()).perform(click());
396        verify(onClickListener, times(2)).onClick(mAlertDialog, currentlyExpectedSelectionIndex);
397        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
398
399        // Now we're going to click the last item and test that the click listener has been invoked
400        // to update the original state array
401        currentlyExpectedSelectionIndex = expectedCount - 1;
402        interactionForClick = onData(allOf(
403                is(instanceOf(SQLiteCursor.class)),
404                TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME,
405                        expectedContent[currentlyExpectedSelectionIndex])));
406        interactionForClick.inRoot(isDialog()).perform(click());
407        verify(onClickListener, times(1)).onClick(mAlertDialog, currentlyExpectedSelectionIndex);
408        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
409    }
410
411    @LargeTest
412    @Test
413    public void testSingleChoiceItemsFromCursor() {
414        mCursor = mDatabase.query("test", mProjectionWithoutChecked,
415                null, null, null, null, null);
416        assertNotNull(mCursor);
417
418        final DialogInterface.OnClickListener mockClickListener =
419                mock(DialogInterface.OnClickListener.class);
420        AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity())
421                .setTitle(R.string.alert_dialog_title)
422                .setSingleChoiceItems(mCursor, 2, TEXT_COLUMN_NAME, mockClickListener);
423        wireBuilder(builder);
424
425        verifySingleChoiceItemsContent(mTextContent, 2, mockClickListener);
426    }
427}
428