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