/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.app; import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.RootMatchers.isDialog; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.core.AllOf.allOf; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.database.Cursor; import android.database.sqlite.SQLiteCursor; import android.database.sqlite.SQLiteDatabase; import android.support.test.espresso.DataInteraction; import android.support.test.filters.MediumTest; import android.support.v7.appcompat.test.R; import android.support.v7.testutils.TestUtilsMatchers; import android.view.View; import android.widget.Button; import android.widget.CheckedTextView; import android.widget.ListAdapter; import android.widget.ListView; import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.File; @MediumTest public class AlertDialogCursorTest extends BaseInstrumentationTestCase<AlertDialogTestActivity> { private Button mButton; private static final String TEXT_COLUMN_NAME = "text"; private static final String CHECKED_COLUMN_NAME = "checked"; private String[] mTextContent; private boolean[] mCheckedContent; private String[] mProjectionWithChecked; private String[] mProjectionWithoutChecked; private SQLiteDatabase mDatabase; private File mDatabaseFile; private Cursor mCursor; private AlertDialog mAlertDialog; public AlertDialogCursorTest() { super(AlertDialogTestActivity.class); } @Before public void setUp() { // Ideally these constant arrays would be defined as final static fields on the // class level, but for some reason those get reset to null on v9- devices after // the first test method has been executed. mTextContent = new String[] { "Adele", "Beyonce", "Ciara", "Dido" }; mCheckedContent = new boolean[] { false, false, true, false }; mProjectionWithChecked = new String[] { "_id", // 0 TEXT_COLUMN_NAME, // 1 CHECKED_COLUMN_NAME // 2 }; mProjectionWithoutChecked = new String[] { "_id", // 0 TEXT_COLUMN_NAME // 1 }; final AlertDialogTestActivity activity = mActivityTestRule.getActivity(); mButton = (Button) activity.findViewById(R.id.test_button); File dbDir = activity.getDir("tests", Context.MODE_PRIVATE); mDatabaseFile = new File(dbDir, "database_alert_dialog_test.db"); if (mDatabaseFile.exists()) { mDatabaseFile.delete(); } mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); assertNotNull(mDatabase); // Create and populate a test table mDatabase.execSQL( "CREATE TABLE test (_id INTEGER PRIMARY KEY, " + TEXT_COLUMN_NAME + " TEXT, " + CHECKED_COLUMN_NAME + " INTEGER);"); for (int i = 0; i < mTextContent.length; i++) { mDatabase.execSQL("INSERT INTO test (" + TEXT_COLUMN_NAME + ", " + CHECKED_COLUMN_NAME + ") VALUES ('" + mTextContent[i] + "', " + (mCheckedContent[i] ? "1" : "0") + ");"); } } @After public void tearDown() throws Throwable { if (mCursor != null) { // Close the cursor on the UI thread as the list view in the alert dialog // will get notified of any change to the underlying cursor. mActivityTestRule.runOnUiThread(new Runnable() { @Override public void run() { mCursor.close(); mCursor = null; } }); } if (mDatabase != null) { mDatabase.close(); } if (mDatabaseFile != null) { mDatabaseFile.delete(); } if (mAlertDialog != null) { mAlertDialog.dismiss(); } } private void wireBuilder(final AlertDialog.Builder builder) { mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mAlertDialog = builder.show(); } }); } private void verifySimpleItemsContent(String[] expectedContent, DialogInterface.OnClickListener onClickListener) { final int expectedCount = expectedContent.length; onView(withId(R.id.test_button)).perform(click()); final ListView listView = mAlertDialog.getListView(); assertNotNull("List view is shown", listView); final ListAdapter listAdapter = listView.getAdapter(); assertEquals("List has " + expectedCount + " entries", expectedCount, listAdapter.getCount()); // Test that all items are showing onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed())); for (int i = 0; i < expectedCount; i++) { DataInteraction rowInteraction = onData(allOf( is(instanceOf(SQLiteCursor.class)), TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[i]))); rowInteraction.inRoot(isDialog()).check(matches(isDisplayed())); } // Verify that our click listener hasn't been called yet verify(onClickListener, never()).onClick(any(DialogInterface.class), any(int.class)); // Test that a click on an item invokes the registered listener int indexToClick = expectedCount - 2; DataInteraction interactionForClick = onData(allOf( is(instanceOf(SQLiteCursor.class)), TestUtilsMatchers.withCursorItemContent( TEXT_COLUMN_NAME, expectedContent[indexToClick]))); interactionForClick.inRoot(isDialog()).perform(click()); verify(onClickListener, times(1)).onClick(mAlertDialog, indexToClick); } @Test public void testSimpleItemsFromCursor() { mCursor = mDatabase.query("test", mProjectionWithoutChecked, null, null, null, null, null); assertNotNull(mCursor); final DialogInterface.OnClickListener mockClickListener = mock(DialogInterface.OnClickListener.class); AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity()) .setTitle(R.string.alert_dialog_title) .setCursor(mCursor, mockClickListener, "text"); wireBuilder(builder); verifySimpleItemsContent(mTextContent, mockClickListener); } /** * Helper method to verify the state of the multi-choice items list. It gets the String * array of content and verifies that: * * 1. The items in the array are rendered as CheckedTextViews inside a ListView * 2. Each item in the array is displayed * 3. Checked state of each row in the ListView corresponds to the matching entry in the * passed boolean array */ private void verifyMultiChoiceItemsState(String[] expectedContent, boolean[] checkedTracker) { final int expectedCount = expectedContent.length; final ListView listView = mAlertDialog.getListView(); assertNotNull("List view is shown", listView); final ListAdapter listAdapter = listView.getAdapter(); assertEquals("List has " + expectedCount + " entries", expectedCount, listAdapter.getCount()); for (int i = 0; i < expectedCount; i++) { Matcher checkedStateMatcher = checkedTracker[i] ? TestUtilsMatchers.isCheckedTextView() : TestUtilsMatchers.isNonCheckedTextView(); // Check that the corresponding row is rendered as CheckedTextView with expected // checked state. DataInteraction rowInteraction = onData(allOf( is(instanceOf(SQLiteCursor.class)), TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[i]))); rowInteraction.inRoot(isDialog()). check(matches(allOf( isDisplayed(), isAssignableFrom(CheckedTextView.class), isDescendantOfA(isAssignableFrom(ListView.class)), checkedStateMatcher))); } } private void verifyMultiChoiceItemsContent(String[] expectedContent, final boolean[] checkedTracker) { final int expectedCount = expectedContent.length; onView(withId(R.id.test_button)).perform(click()); final ListView listView = mAlertDialog.getListView(); assertNotNull("List view is shown", listView); final ListAdapter listAdapter = listView.getAdapter(); assertEquals("List has " + expectedCount + " entries", expectedCount, listAdapter.getCount()); // Test that all items are showing onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed())); verifyMultiChoiceItemsState(expectedContent, checkedTracker); // We're going to click item #1 and test that the click listener has been invoked to // update the original state array boolean[] expectedAfterClick1 = checkedTracker.clone(); expectedAfterClick1[1] = !expectedAfterClick1[1]; DataInteraction interactionForClick = onData(allOf( is(instanceOf(SQLiteCursor.class)), TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[1]))); interactionForClick.inRoot(isDialog()).perform(click()); verifyMultiChoiceItemsState(expectedContent, expectedAfterClick1); // Now click item #1 again and test that the click listener has been invoked to update the // original state array again expectedAfterClick1[1] = !expectedAfterClick1[1]; interactionForClick.inRoot(isDialog()).perform(click()); verifyMultiChoiceItemsState(expectedContent, expectedAfterClick1); // Now we're going to click the last item and test that the click listener has been invoked // to update the original state array boolean[] expectedAfterClickLast = checkedTracker.clone(); expectedAfterClickLast[expectedCount - 1] = !expectedAfterClickLast[expectedCount - 1]; interactionForClick = onData(allOf( is(instanceOf(SQLiteCursor.class)), TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[expectedCount - 1]))); interactionForClick.inRoot(isDialog()).perform(click()); verifyMultiChoiceItemsState(expectedContent, expectedAfterClickLast); } @Test public void testMultiChoiceItemsFromCursor() { mCursor = mDatabase.query("test", mProjectionWithChecked, null, null, null, null, null); assertNotNull(mCursor); final boolean[] checkedTracker = mCheckedContent.clone(); AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity()) .setTitle(R.string.alert_dialog_title) .setMultiChoiceItems(mCursor, CHECKED_COLUMN_NAME, TEXT_COLUMN_NAME, new DialogInterface.OnMultiChoiceClickListener() { @Override public void onClick(DialogInterface dialog, int which, boolean isChecked) { // Update the underlying database with the new checked // state for the specific row mCursor.moveToPosition(which); ContentValues valuesToUpdate = new ContentValues(); valuesToUpdate.put(CHECKED_COLUMN_NAME, isChecked ? 1 : 0); mDatabase.update("test", valuesToUpdate, TEXT_COLUMN_NAME + " = ?", new String[] { mCursor.getString(1) } ); mCursor.requery(); checkedTracker[which] = isChecked; } }); wireBuilder(builder); // Pass the same boolean[] array as used for initialization since our click listener // will be updating its content. verifyMultiChoiceItemsContent(mTextContent, checkedTracker); } /** * Helper method to verify the state of the single-choice items list. It gets the String * array of content and verifies that: * * 1. The items in the array are rendered as CheckedTextViews inside a ListView * 2. Each item in the array is displayed * 3. Only one row in the ListView is checked, and that corresponds to the passed * integer index. */ private void verifySingleChoiceItemsState(String[] expectedContent, int currentlyExpectedSelectionIndex) { final int expectedCount = expectedContent.length; final ListView listView = mAlertDialog.getListView(); assertNotNull("List view is shown", listView); final ListAdapter listAdapter = listView.getAdapter(); assertEquals("List has " + expectedCount + " entries", expectedCount, listAdapter.getCount()); for (int i = 0; i < expectedCount; i++) { Matcher checkedStateMatcher = (i == currentlyExpectedSelectionIndex) ? TestUtilsMatchers.isCheckedTextView() : TestUtilsMatchers.isNonCheckedTextView(); // Check that the corresponding row is rendered as CheckedTextView with expected // checked state. DataInteraction rowInteraction = onData(allOf( is(instanceOf(SQLiteCursor.class)), TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[i]))); rowInteraction.inRoot(isDialog()). check(matches(allOf( isDisplayed(), isAssignableFrom(CheckedTextView.class), isDescendantOfA(isAssignableFrom(ListView.class)), checkedStateMatcher))); } } private void verifySingleChoiceItemsContent(String[] expectedContent, int initialSelectionIndex, DialogInterface.OnClickListener onClickListener) { final int expectedCount = expectedContent.length; int currentlyExpectedSelectionIndex = initialSelectionIndex; onView(withId(R.id.test_button)).perform(click()); // Test that all items are showing onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed())); verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex); // We're going to click the first unselected item and test that the click listener has // been invoked. currentlyExpectedSelectionIndex = (currentlyExpectedSelectionIndex == 0) ? 1 : 0; DataInteraction interactionForClick = onData(allOf( is(instanceOf(SQLiteCursor.class)), TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[currentlyExpectedSelectionIndex]))); interactionForClick.inRoot(isDialog()).perform(click()); verify(onClickListener, times(1)).onClick(mAlertDialog, currentlyExpectedSelectionIndex); verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex); // Now click the same item again and test that the selection has not changed interactionForClick.inRoot(isDialog()).perform(click()); verify(onClickListener, times(2)).onClick(mAlertDialog, currentlyExpectedSelectionIndex); verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex); // Now we're going to click the last item and test that the click listener has been invoked // to update the original state array currentlyExpectedSelectionIndex = expectedCount - 1; interactionForClick = onData(allOf( is(instanceOf(SQLiteCursor.class)), TestUtilsMatchers.withCursorItemContent(TEXT_COLUMN_NAME, expectedContent[currentlyExpectedSelectionIndex]))); interactionForClick.inRoot(isDialog()).perform(click()); verify(onClickListener, times(1)).onClick(mAlertDialog, currentlyExpectedSelectionIndex); verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex); } @Test public void testSingleChoiceItemsFromCursor() { mCursor = mDatabase.query("test", mProjectionWithoutChecked, null, null, null, null, null); assertNotNull(mCursor); final DialogInterface.OnClickListener mockClickListener = mock(DialogInterface.OnClickListener.class); AlertDialog.Builder builder = new AlertDialog.Builder(mActivityTestRule.getActivity()) .setTitle(R.string.alert_dialog_title) .setSingleChoiceItems(mCursor, 2, TEXT_COLUMN_NAME, mockClickListener); wireBuilder(builder); verifySingleChoiceItemsContent(mTextContent, 2, mockClickListener); } }