| /* |
| * Copyright (C) 2016 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.design.widget; |
| |
| import static android.support.design.testutils.TestUtilsActions.setLayoutDirection; |
| import static android.support.test.espresso.Espresso.onView; |
| import static android.support.test.espresso.action.ViewActions.click; |
| import static android.support.test.espresso.action.ViewActions.swipeLeft; |
| import static android.support.test.espresso.action.ViewActions.swipeRight; |
| import static android.support.test.espresso.assertion.ViewAssertions.matches; |
| import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; |
| import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; |
| import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; |
| import static android.support.test.espresso.matcher.ViewMatchers.withId; |
| import static android.support.test.espresso.matcher.ViewMatchers.withText; |
| |
| import static org.hamcrest.core.AllOf.allOf; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.Matchers.any; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.spy; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyNoMoreInteractions; |
| |
| import android.content.res.Resources; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.StringRes; |
| import android.support.design.test.R; |
| import android.support.design.testutils.SnackbarUtils; |
| import android.support.test.espresso.ViewAction; |
| import android.support.test.espresso.ViewInteraction; |
| import android.support.test.espresso.action.CoordinatesProvider; |
| import android.support.test.espresso.action.GeneralSwipeAction; |
| import android.support.test.espresso.action.Press; |
| import android.support.test.espresso.action.Swipe; |
| import android.support.test.filters.MediumTest; |
| import android.support.v4.view.ViewCompat; |
| import android.text.TextUtils; |
| import android.view.View; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| @MediumTest |
| public class SnackbarTest extends BaseInstrumentationTestCase<SnackbarActivity> { |
| private static final String MESSAGE_TEXT = "Test Message"; |
| private static final @StringRes int MESSAGE_ID = R.string.snackbar_text; |
| private static final String ACTION_TEXT = "Action"; |
| private static final @StringRes int ACTION_ID = R.string.snackbar_action; |
| |
| private CoordinatorLayout mCoordinatorLayout; |
| |
| private interface DismissAction { |
| void dismiss(Snackbar snackbar); |
| } |
| |
| public SnackbarTest() { |
| super(SnackbarActivity.class); |
| } |
| |
| @Before |
| public void setup() { |
| mCoordinatorLayout = |
| (CoordinatorLayout) mActivityTestRule.getActivity().findViewById(R.id.col); |
| } |
| |
| private void verifySnackbarContent(final Snackbar snackbar, final String expectedMessage, |
| final String expectedAction) throws Throwable { |
| // Show the snackbar |
| SnackbarUtils.showTransientBottomBarAndWaitUntilFullyShown(snackbar); |
| |
| // Verify that we're showing the message |
| withText(expectedMessage).matches(allOf( |
| isDescendantOfA(isAssignableFrom(Snackbar.SnackbarLayout.class)), |
| isCompletelyDisplayed())); |
| |
| // If the action is not empty, verify that we're showing it |
| if (!TextUtils.isEmpty(expectedAction)) { |
| withText(expectedAction).matches(allOf( |
| isDescendantOfA(isAssignableFrom(Snackbar.SnackbarLayout.class)), |
| isCompletelyDisplayed())); |
| } |
| |
| // Dismiss the snackbar |
| SnackbarUtils.dismissTransientBottomBarAndWaitUntilFullyDismissed(snackbar); |
| } |
| |
| @Test |
| public void testBasicContent() throws Throwable { |
| // Verify different combinations of snackbar content (message and action) and duration |
| |
| final Resources res = mActivityTestRule.getActivity().getResources(); |
| final String resolvedMessage = res.getString(MESSAGE_ID); |
| final String resolvedAction = res.getString(ACTION_ID); |
| |
| // String message and no action |
| verifySnackbarContent( |
| Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, Snackbar.LENGTH_SHORT), |
| MESSAGE_TEXT, null); |
| |
| // Resource message and no action |
| verifySnackbarContent( |
| Snackbar.make(mCoordinatorLayout, MESSAGE_ID, Snackbar.LENGTH_LONG), |
| resolvedMessage, null); |
| |
| // String message and string action |
| verifySnackbarContent( |
| Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, Snackbar.LENGTH_INDEFINITE) |
| .setAction(ACTION_TEXT, mock(View.OnClickListener.class)), |
| MESSAGE_TEXT, ACTION_TEXT); |
| |
| // String message and resource action |
| verifySnackbarContent( |
| Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, Snackbar.LENGTH_SHORT) |
| .setAction(ACTION_ID, mock(View.OnClickListener.class)), |
| MESSAGE_TEXT, resolvedAction); |
| |
| // Resource message and resource action |
| verifySnackbarContent( |
| Snackbar.make(mCoordinatorLayout, MESSAGE_ID, Snackbar.LENGTH_LONG) |
| .setAction(ACTION_ID, mock(View.OnClickListener.class)), |
| resolvedMessage, resolvedAction); |
| } |
| |
| private void verifyDismissCallback(final ViewInteraction interaction, |
| final @Nullable ViewAction action, final @Nullable DismissAction dismissAction, |
| final int length, @Snackbar.Callback.DismissEvent final int expectedEvent) |
| throws Throwable { |
| final BaseTransientBottomBar.BaseCallback mockCallback = |
| mock(BaseTransientBottomBar.BaseCallback.class); |
| final Snackbar snackbar = Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, length) |
| .setAction(ACTION_TEXT, mock(View.OnClickListener.class)) |
| .addCallback(mockCallback); |
| |
| // Show the snackbar |
| SnackbarUtils.showTransientBottomBarAndWaitUntilFullyShown(snackbar); |
| // Verify that our onShown has been called |
| verify(mockCallback, times(1)).onShown(snackbar); |
| // and that the snackbar is either shown or queued to be shown |
| assertTrue(snackbar.isShownOrQueued()); |
| // and also check that we have the intended message / action displayed somewhere in |
| // our hierarchy |
| onView(withText(MESSAGE_TEXT)).check(matches(isCompletelyDisplayed())); |
| onView(withText(ACTION_TEXT)).check(matches(isCompletelyDisplayed())); |
| |
| // Now perform the UI interaction |
| SnackbarUtils.performActionAndWaitUntilFullyDismissed(snackbar, |
| new SnackbarUtils.TransientBottomBarAction() { |
| @Override |
| public void perform() throws Throwable { |
| if (action != null) { |
| interaction.perform(action); |
| } else if (dismissAction != null) { |
| mActivityTestRule.runOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| dismissAction.dismiss(snackbar); |
| } |
| }); |
| } |
| } |
| }); |
| |
| // Verify that our onDismissed has been called |
| verify(mockCallback, times(1)).onDismissed(snackbar, expectedEvent); |
| verifyNoMoreInteractions(mockCallback); |
| // and that the snackbar is neither shown nor queued to be shown |
| assertFalse(snackbar.isShownOrQueued()); |
| } |
| |
| @Test |
| public void testDismissViaActionClick() throws Throwable { |
| verifyDismissCallback( |
| onView(withId(R.id.snackbar_action)), |
| click(), |
| null, |
| Snackbar.LENGTH_LONG, |
| Snackbar.Callback.DISMISS_EVENT_ACTION); |
| } |
| |
| @Test |
| public void testDismissViaSwipe() throws Throwable { |
| verifyDismissCallback( |
| onView(isAssignableFrom(Snackbar.SnackbarLayout.class)), |
| swipeRight(), |
| null, |
| Snackbar.LENGTH_LONG, |
| Snackbar.Callback.DISMISS_EVENT_SWIPE); |
| } |
| |
| @Test |
| public void testDismissViaSwipeRtl() throws Throwable { |
| onView(withId(R.id.col)).perform(setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL)); |
| if (ViewCompat.getLayoutDirection(mCoordinatorLayout) == ViewCompat.LAYOUT_DIRECTION_RTL) { |
| // On devices that support RTL layout, the start-to-end dismiss swipe is done |
| // with swipeLeft() action |
| verifyDismissCallback( |
| onView(isAssignableFrom(Snackbar.SnackbarLayout.class)), |
| swipeLeft(), |
| null, |
| Snackbar.LENGTH_LONG, |
| Snackbar.Callback.DISMISS_EVENT_SWIPE); |
| } |
| } |
| |
| @Test |
| public void testDismissViaApi() throws Throwable { |
| verifyDismissCallback( |
| onView(isAssignableFrom(Snackbar.SnackbarLayout.class)), |
| null, |
| new DismissAction() { |
| @Override |
| public void dismiss(Snackbar snackbar) { |
| snackbar.dismiss(); |
| } |
| }, |
| Snackbar.LENGTH_LONG, |
| Snackbar.Callback.DISMISS_EVENT_MANUAL); |
| } |
| |
| @Test |
| public void testDismissViaTimeout() throws Throwable { |
| verifyDismissCallback( |
| onView(isAssignableFrom(Snackbar.SnackbarLayout.class)), |
| null, |
| null, |
| Snackbar.LENGTH_LONG, |
| Snackbar.Callback.DISMISS_EVENT_TIMEOUT); |
| } |
| |
| @Test |
| public void testSwipeUpDismissesViaTimeout() throws Throwable { |
| verifyDismissCallback( |
| onView(isAssignableFrom(Snackbar.SnackbarLayout.class)), |
| // This is a swipe up, from the middle center of the view, to above the view |
| // (outside the bounds) |
| new GeneralSwipeAction( |
| Swipe.SLOW, |
| new CoordinatesProvider() { |
| @Override |
| public float[] calculateCoordinates(View view) { |
| final int[] loc = new int[2]; |
| view.getLocationOnScreen(loc); |
| return new float[]{ |
| loc[0] + view.getWidth() / 2, |
| loc[1] + view.getHeight() / 2}; |
| } |
| }, |
| new CoordinatesProvider() { |
| @Override |
| public float[] calculateCoordinates(View view) { |
| final int[] loc = new int[2]; |
| view.getLocationOnScreen(loc); |
| return new float[]{ |
| loc[0] + view.getWidth() / 2, |
| loc[1] - view.getHeight()}; |
| } |
| }, |
| Press.FINGER |
| ), |
| null, |
| Snackbar.LENGTH_SHORT, |
| Snackbar.Callback.DISMISS_EVENT_TIMEOUT); |
| } |
| |
| @Test |
| public void testDismissViaAnotherSnackbar() throws Throwable { |
| final Snackbar anotherSnackbar = |
| Snackbar.make(mCoordinatorLayout, "A different message", Snackbar.LENGTH_SHORT); |
| |
| // Our dismiss action is to show another snackbar (and verify that the original snackbar |
| // is now dismissed with CONSECUTIVE event) |
| verifyDismissCallback( |
| onView(isAssignableFrom(Snackbar.SnackbarLayout.class)), |
| null, |
| new DismissAction() { |
| @Override |
| public void dismiss(Snackbar snackbar) { |
| anotherSnackbar.show(); |
| } |
| }, |
| Snackbar.LENGTH_LONG, |
| Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE); |
| |
| // And dismiss the second snackbar to get back to clean state |
| SnackbarUtils.dismissTransientBottomBarAndWaitUntilFullyDismissed(anotherSnackbar); |
| } |
| |
| @Test |
| public void testActionClickListener() { |
| final View.OnClickListener mockClickListener = mock(View.OnClickListener.class); |
| final Snackbar snackbar = |
| Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, Snackbar.LENGTH_SHORT) |
| .setAction(ACTION_TEXT, mockClickListener); |
| |
| // Show the snackbar |
| SnackbarUtils.showTransientBottomBarAndWaitUntilFullyShown(snackbar); |
| // perform the action click |
| onView(withId(R.id.snackbar_action)).perform(click()); |
| // and verify that our click listener has been called |
| verify(mockClickListener, times(1)).onClick(any(View.class)); |
| } |
| |
| @Test |
| public void testSetCallback() throws Throwable { |
| final Snackbar snackbar = |
| Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, Snackbar.LENGTH_INDEFINITE) |
| .setAction(ACTION_TEXT, mock(View.OnClickListener.class)); |
| final Snackbar.Callback mockCallback = spy(new Snackbar.Callback()); |
| snackbar.setCallback(mockCallback); |
| |
| SnackbarUtils.showTransientBottomBarAndWaitUntilFullyShown(snackbar); |
| verify(mockCallback, times(1)).onShown(snackbar); |
| |
| SnackbarUtils.dismissTransientBottomBarAndWaitUntilFullyDismissed(snackbar); |
| verify(mockCallback, times(1)).onDismissed(snackbar, |
| BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL); |
| } |
| |
| @Test |
| public void testSingleCallback() throws Throwable { |
| final Snackbar snackbar = |
| Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, Snackbar.LENGTH_INDEFINITE) |
| .setAction(ACTION_TEXT, mock(View.OnClickListener.class)); |
| final BaseTransientBottomBar.BaseCallback mockCallback1 = |
| mock(BaseTransientBottomBar.BaseCallback.class); |
| final BaseTransientBottomBar.BaseCallback mockCallback2 = |
| mock(BaseTransientBottomBar.BaseCallback.class); |
| snackbar.addCallback(mockCallback1); |
| |
| SnackbarUtils.showTransientBottomBarAndWaitUntilFullyShown(snackbar); |
| verify(mockCallback1, times(1)).onShown(snackbar); |
| verify(mockCallback2, never()).onShown(snackbar); |
| |
| SnackbarUtils.dismissTransientBottomBarAndWaitUntilFullyDismissed(snackbar); |
| verify(mockCallback1, times(1)).onDismissed(snackbar, |
| BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL); |
| verify(mockCallback2, never()).onDismissed(snackbar, |
| BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL); |
| } |
| |
| @Test |
| public void testMultipleCallbacks() throws Throwable { |
| final Snackbar snackbar = |
| Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, Snackbar.LENGTH_INDEFINITE) |
| .setAction(ACTION_TEXT, mock(View.OnClickListener.class)); |
| final BaseTransientBottomBar.BaseCallback mockCallback1 = |
| mock(BaseTransientBottomBar.BaseCallback.class); |
| final BaseTransientBottomBar.BaseCallback mockCallback2 = |
| mock(BaseTransientBottomBar.BaseCallback.class); |
| snackbar.addCallback(mockCallback1); |
| snackbar.addCallback(mockCallback2); |
| |
| SnackbarUtils.showTransientBottomBarAndWaitUntilFullyShown(snackbar); |
| verify(mockCallback1, times(1)).onShown(snackbar); |
| verify(mockCallback2, times(1)).onShown(snackbar); |
| |
| SnackbarUtils.dismissTransientBottomBarAndWaitUntilFullyDismissed(snackbar); |
| verify(mockCallback1, times(1)).onDismissed(snackbar, |
| BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL); |
| verify(mockCallback2, times(1)).onDismissed(snackbar, |
| BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL); |
| } |
| |
| @Test |
| public void testMultipleCallbacksWithRemoval() throws Throwable { |
| final Snackbar snackbar = |
| Snackbar.make(mCoordinatorLayout, MESSAGE_TEXT, Snackbar.LENGTH_INDEFINITE) |
| .setAction(ACTION_TEXT, mock(View.OnClickListener.class)); |
| final BaseTransientBottomBar.BaseCallback mockCallback1 = |
| mock(BaseTransientBottomBar.BaseCallback.class); |
| final BaseTransientBottomBar.BaseCallback mockCallback2 = |
| mock(BaseTransientBottomBar.BaseCallback.class); |
| snackbar.addCallback(mockCallback1); |
| snackbar.addCallback(mockCallback2); |
| |
| SnackbarUtils.showTransientBottomBarAndWaitUntilFullyShown(snackbar); |
| verify(mockCallback1, times(1)).onShown(snackbar); |
| verify(mockCallback2, times(1)).onShown(snackbar); |
| |
| snackbar.removeCallback(mockCallback2); |
| |
| SnackbarUtils.dismissTransientBottomBarAndWaitUntilFullyDismissed(snackbar); |
| verify(mockCallback1, times(1)).onDismissed(snackbar, |
| BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL); |
| verify(mockCallback2, never()).onDismissed(snackbar, |
| BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL); |
| } |
| } |