/* * Copyright 2016 Martijn Brekhof. All rights reserved. * * 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 org.xbmc.kore.testhelpers; import android.app.Activity; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.view.View; import android.widget.AutoCompleteTextView; import android.widget.TextView; import androidx.test.espresso.Espresso; import androidx.test.espresso.NoMatchingViewException; import androidx.test.espresso.UiController; import androidx.test.espresso.ViewAction; import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.contrib.RecyclerViewActions; import com.sothree.slidinguppanel.SlidingUpPanelLayout; import org.hamcrest.Matcher; import org.xbmc.kore.R; import org.xbmc.kore.testhelpers.action.ViewActions; import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; import static androidx.test.espresso.Espresso.pressBack; import static androidx.test.espresso.action.ViewActions.clearText; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withParent; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.CoreMatchers.anything; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.xbmc.kore.testhelpers.action.ViewActions.clearFocus; public class EspressoTestUtils { public static void rotateDevice(Activity activity) { int orientation = activity.getResources().getConfiguration().orientation; activity.setRequestedOrientation( (orientation == Configuration.ORIENTATION_PORTRAIT) ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } /** * Clicks a menu item regardless if it is in the overflow menu or * visible as icon in the action bar * @param activity * @param name Name of the menu item in the overflow menu * @param resourceId Resource identifier of the menu item */ public static void clickMenuItem(Activity activity, String name, int resourceId) { try { onView(withId(resourceId)).perform(click()); } catch (NoMatchingViewException e) { openActionBarOverflowOrOptionsMenu(activity); //Use onData as item might not be visible in the View without scrolling onData(allOf( Matchers.withMenuTitle(name))) .perform(click()); } } /** * Clicks the arrow button in the toolbar when its function is collapsing a view. For instance, * collapse the search view in the toolbar. */ public static void clickToolbarCollapseButton() { /** * The image button in the toolbar used as home/collapse/back button has no ID we can use. * In appcompat v7 the arrow button in the toolbar used to collapse a search view has a * description "Collapse". We use this to find the button in the view and perform the click * action. */ onView(withContentDescription("Collapse")).perform(click()); } /** * Clicks the button with given resourceId and checks if the * button is displayed. As we occasionally use the same identifiers * in multiple fragments, we need to check if it is visible as well * to prevent Espresso from finding multiple views that match the * resource identifier. * @param resourceId */ public static void clickButton(int resourceId) { onView(allOf(withId(resourceId), isDisplayed())).perform(click()); } /** * Clicks on the search menu item and enters the given search query * @param activity * @param query */ public static void enterSearchQuery(Activity activity, String query) { EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search); onView(isAssignableFrom(AutoCompleteTextView.class)) .perform(click(), typeText(query), closeSoftKeyboard()); onView(isRoot()).perform(clearFocus()); } /** * Clicks on the search menu item and clears the search query by entering the empty string * @param activity */ public static void clearSearchQuery(Activity activity) { EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search); onView(isAssignableFrom(AutoCompleteTextView.class)) .perform(click(), clearText(), closeSoftKeyboard()); } /** * Clears the search query by pressing the X button * @param activity */ public static void clearSearchQueryXButton(Activity activity) { try { onView(withId(R.id.search_close_btn)).perform(click()); } catch (NoMatchingViewException e) { EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search); onView(withId(R.id.search_close_btn)).perform(click()); } Espresso.closeSoftKeyboard(); } /** * Performs a click on an item in an adapter view, such as GridView or ListView * @param position * @param resourceId of adapter view holding the item that should be clicked */ public static void clickAdapterViewItem(int position, int resourceId) { onData(anything()).inAdapterView(allOf(withId(resourceId), isDisplayed())) .atPosition(position).perform(click()); } public static void clickRecyclerViewItem(int position, int resourceId) { onView(withId(resourceId)).perform(RecyclerViewActions.actionOnItemAtPosition(position, click())); } public static void clickRecyclerViewItem(String text, int resourceId) { ViewInteraction viewInteraction = onView(allOf(withId(resourceId), hasDescendant(withText(containsString(text))), isDisplayed())); viewInteraction.perform(RecyclerViewActions.scrollTo(hasDescendant(withText(containsString(text))))); viewInteraction.perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(containsString(text))), click())); } /** * Checks that SearchView contains the given text * @param query text that SearchView should contain */ public static void checkTextInSearchQuery(String query) { onView(isAssignableFrom(AutoCompleteTextView.class)).check(matches(withText(query))); } /** * Checks that the list contains item(s) matching search query * @param query text each element must contain * @param listSize amount of elements expected in list * @param resourceId resource identifier or list view */ public static void checkListMatchesSearchQuery(String query, int listSize, int resourceId) { onView(isRoot()).perform(ViewActions.waitForView(resourceId, new ViewActions.CheckStatus() { @Override public boolean check(View v) { return v.isShown(); } }, 10000)); onView(allOf(withId(resourceId), isDisplayed())) .check(matches(Matchers.withOnlyMatchingDataItems(hasDescendant(withText(containsString(query)))))); checkRecyclerViewListsize(listSize, resourceId); } /** * Checks that the list size matches the given list size * @param listSize amount of elements expected in list */ public static void checkRecyclerViewListsize(int listSize, int resourceId) { onView(allOf(withId(resourceId), isDisplayed())) .check(matches(Matchers.withRecyclerViewSize(listSize))); } /** * Checks that the list size matches the given list size * @param listSize amount of elements expected in list */ public static void checkListViewSize(int listSize, int resourceId) { onView(allOf(withId(resourceId), isDisplayed())) .check(matches(Matchers.withListViewSize(listSize))); } /** * Checks if search action view does not exist in the current view hierarchy */ public static void checkSearchMenuCollapsed() { onView(isAssignableFrom(AutoCompleteTextView.class)).check(doesNotExist()); } /** * Returns the current active activity. Use this when the originally started activity * started a new activity and you need the reference to the new activity. * @return reference to the current active activity */ public static Activity getActivity() { final Activity[] activity = new Activity[1]; onView(allOf(withId(android.R.id.content), isDisplayed())).perform(new ViewAction() { @Override public Matcher getConstraints() { return isAssignableFrom(View.class); } @Override public String getDescription() { return "getting current activity"; } @Override public void perform(UiController uiController, View view) { if (view.getContext() instanceof Activity) { activity[0] = ((Activity)view.getContext()); } } }); return activity[0]; } /** * Clicks on tab that contains the text given by stringResourceId. * @param stringResourceId text displayed in Tab that should be clicked */ public static void clickTab(int stringResourceId) { onView(withId(R.id.pager)).perform(ViewActions.setCurrentViewPagerItem(stringResourceId)); } /** * Clicks the album tab in the music activity */ public static void clickAlbumsTab() { clickTab(R.string.albums); } /** * Clicks the artists tab in the music activity */ public static void clickArtistsTab() { clickTab(R.string.artists); } /** * Clicks the genres tab in the music activity */ public static void clickGenresTab() { clickTab(R.string.genres); } /** * Clicks the music videos tab in the music activity */ public static void clickMusicVideosTab() { clickTab(R.string.videos); } /** * Selects an item in the list, then presses back and checks the action bar title * @param item number (0 is first item) of the item that should be pressed * @param listResourceId Resource identifier of the AdapterView * @param actionbarTitle title that should be displayed in the action bar after pressing back */ public static void selectListItemPressBackAndCheckActionbarTitle(int item, int listResourceId, String actionbarTitle) { EspressoTestUtils.clickRecyclerViewItem(item, listResourceId); pressBack(); onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar)))) .check(matches(withText(actionbarTitle))); } /** * Selects an item in the list, then presses back and checks the action bar title * @param itemText the text the item that must be pressed should contain * @param listResourceId Resource identifier of the AdapterView * @param actionbarTitle title that should be displayed in the action bar after pressing back */ public static void selectListItemPressBackAndCheckActionbarTitle(String itemText, int listResourceId, String actionbarTitle) { EspressoTestUtils.clickRecyclerViewItem(itemText, listResourceId); pressBack(); onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar)))) .check(matches(withText(containsString(actionbarTitle)))); } /** * Selects an item in the list, then rotates the device and checks the action bar title * @param itemText the text the item that must be pressed should contain * @param listResourceId Resource identifier of the AdapterView * @param actionbarTitle title that should be displayed in the action bar after rotating */ public static void selectListItemRotateDeviceAndCheckActionbarTitle(String itemText, int listResourceId, final String actionbarTitle, Activity activity) { EspressoTestUtils.clickRecyclerViewItem(itemText, listResourceId); EspressoTestUtils.rotateDevice(activity); onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar)))) .check(matches(withText(containsString(actionbarTitle)))); } /** * Selects an item in the list and then checks the action bar title * @param itemText the text the item that must be pressed should contain * @param listResourceId Resource identifier of the AdapterView * @param actionbarTitle title that should be displayed in the action bar after selecting item */ public static void selectListItemAndCheckActionbarTitle(String itemText, int listResourceId, String actionbarTitle) { EspressoTestUtils.clickRecyclerViewItem(itemText, listResourceId); onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar)))) .check(matches(withText(actionbarTitle))); } /** * Waits for 10 seconds till panel has given state. * * @param panelState desired state of panel */ public static void waitForPanelState(final SlidingUpPanelLayout.PanelState panelState) { onView(isRoot()).perform(ViewActions.waitForView(R.id.now_playing_panel, new ViewActions.CheckStatus() { @Override public boolean check(View v) { return ((SlidingUpPanelLayout) v).getPanelState() == panelState; } }, 10000)); } }