Implemented integration tests to test restoring search query

* Added perl tools to generate json files for music and movie data
* Upgraded build tools version, sdk compile version, and support libraries
  as this is required by the test packages.
* Added new product flavor instrumentationTest to make sure assets and settings
  required for testing do not also ship with a release version of Kore
This commit is contained in:
Martijn Brekhof 2016-03-15 14:42:53 +01:00
parent e338636bb7
commit 6f60442e7b
25 changed files with 116502 additions and 20 deletions

View File

@ -38,6 +38,13 @@ android {
}
}
productFlavors {
instrumentationTest {
applicationId "org.xbmc.kore.instrumentationtest"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
buildTypes {
// debug {
// minifyEnabled true
@ -94,5 +101,32 @@ dependencies {
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
compile 'com.melnykov:floatingactionbutton:1.3.0'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
androidTestCompile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2'
androidTestCompile 'com.android.support:support-v13:23.4.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
}
// Get the path to ADB. Required when running tests directly from Android Studio.
// Source: http://stackoverflow.com/a/26771087/112705
def adb = android.getAdbExe().toString()
// Source: http://stackoverflow.com/q/29908110/112705
afterEvaluate {
task grantAnimationPermissionDev(type: Exec, dependsOn: 'installInstrumentationTestDebug') {
println("Executing: $adb shell pm grant $android.productFlavors.instrumentationTest.applicationId android.permission.SET_ANIMATION_SCALE")
commandLine "$adb shell pm grant $android.productFlavors.instrumentationTest.applicationId android.permission.SET_ANIMATION_SCALE".split(' ')
}
// When launching individual tests from Android Studio, it seems that only the assemble tasks
// get called directly, not the install* versions
tasks.each { task ->
if (task.name.startsWith('assembleInstrumentationTestDebugAndroidTest')) {
task.dependsOn grantAnimationPermissionDev
}
}
}

View File

@ -1,13 +0,0 @@
package org.xbmc.kore;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}

View File

@ -0,0 +1,181 @@
/*
* 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.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiException;
import org.xbmc.kore.jsonrpc.method.AudioLibrary;
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
import org.xbmc.kore.jsonrpc.type.AudioType;
import org.xbmc.kore.jsonrpc.type.LibraryType;
import org.xbmc.kore.jsonrpc.type.VideoType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaProvider;
import org.xbmc.kore.service.library.SyncUtils;
import org.xbmc.kore.utils.LogUtils;
import java.io.IOException;
import java.util.ArrayList;
public class Database {
public static final String TAG = LogUtils.makeLogTag(Database.class);
public static HostInfo fill(Context context) throws ApiException, IOException {
MediaProvider mediaProvider = new MediaProvider();
mediaProvider.setContext(context);
mediaProvider.onCreate();
HostInfo hostInfo = addHost(context);
insertMovies(context, hostInfo.getId());
insertArtists(context, hostInfo.getId());
insertGenres(context, hostInfo.getId());
insertAlbums(context, hostInfo.getId());
insertSongs(context, hostInfo.getId());
return hostInfo;
}
public static void flush(Context context, HostInfo hostInfo) {
context.getContentResolver()
.delete(MediaContract.Hosts.buildHostUri(hostInfo.getId()), null, null);
}
private static HostInfo addHost(Context context) {
return HostManager.getInstance(context).addHost("TestHost", "127.0.0.1", 1, 80, 9090, null, null, "52:54:00:12:35:02", 9, false, 9777);
}
private static void insertMovies(Context context, int hostId) throws ApiException, IOException {
VideoLibrary.GetMovies getMovies = new VideoLibrary.GetMovies();
String result = Utils.readFile(context, "Video.Details.Movie.json");
ArrayList<VideoType.DetailsMovie> movieList = (ArrayList) getMovies.resultFromJson(result);
ContentValues movieValuesBatch[] = new ContentValues[movieList.size()];
int castCount = 0;
// Iterate on each movie
for (int i = 0; i < movieList.size(); i++) {
VideoType.DetailsMovie movie = movieList.get(i);
movieValuesBatch[i] = SyncUtils.contentValuesFromMovie(hostId, movie);
castCount += movie.cast.size();
}
context.getContentResolver().bulkInsert(MediaContract.Movies.CONTENT_URI, movieValuesBatch);
ContentValues movieCastValuesBatch[] = new ContentValues[castCount];
int count = 0;
// Iterate on each movie/cast
for (VideoType.DetailsMovie movie : movieList) {
for (VideoType.Cast cast : movie.cast) {
movieCastValuesBatch[count] = SyncUtils.contentValuesFromCast(hostId, cast);
movieCastValuesBatch[count].put(MediaContract.MovieCastColumns.MOVIEID, movie.movieid);
count++;
}
}
context.getContentResolver().bulkInsert(MediaContract.MovieCast.CONTENT_URI, movieCastValuesBatch);
}
private static void insertArtists(Context context, int hostId) throws ApiException, IOException {
AudioLibrary.GetArtists getArtists = new AudioLibrary.GetArtists(false);
String result = Utils.readFile(context, "AudioLibrary.GetArtists.json");
ArrayList<AudioType.DetailsArtist> artistList = (ArrayList) getArtists.resultFromJson(result).items;
ContentValues artistValuesBatch[] = new ContentValues[artistList.size()];
for (int i = 0; i < artistList.size(); i++) {
AudioType.DetailsArtist artist = artistList.get(i);
artistValuesBatch[i] = SyncUtils.contentValuesFromArtist(hostId, artist);
}
context.getContentResolver().bulkInsert(MediaContract.Artists.CONTENT_URI, artistValuesBatch);
}
private static void insertGenres(Context context, int hostId) throws ApiException, IOException {
AudioLibrary.GetGenres getGenres = new AudioLibrary.GetGenres();
ArrayList<LibraryType.DetailsGenre> genreList = (ArrayList) getGenres.resultFromJson(Utils.readFile(context, "AudioLibrary.GetGenres.json"));
ContentValues genresValuesBatch[] = new ContentValues[genreList.size()];
for (int i = 0; i < genreList.size(); i++) {
LibraryType.DetailsGenre genre = genreList.get(i);
genresValuesBatch[i] = SyncUtils.contentValuesFromAudioGenre(hostId, genre);
}
context.getContentResolver().bulkInsert(MediaContract.AudioGenres.CONTENT_URI, genresValuesBatch);
}
private static void insertAlbums(Context context, int hostId) throws ApiException, IOException {
AudioLibrary.GetAlbums getAlbums = new AudioLibrary.GetAlbums();
String result = Utils.readFile(context, "AudioLibrary.GetAlbums.json");
ArrayList<AudioType.DetailsAlbum> albumList = (ArrayList) getAlbums.resultFromJson(result).items;
ContentResolver contentResolver = context.getContentResolver();
ContentValues albumValuesBatch[] = new ContentValues[albumList.size()];
int artistsCount = 0, genresCount = 0;
for (int i = 0; i < albumList.size(); i++) {
AudioType.DetailsAlbum album = albumList.get(i);
albumValuesBatch[i] = SyncUtils.contentValuesFromAlbum(hostId, album);
artistsCount += album.artistid.size();
genresCount += album.genreid.size();
}
contentResolver.bulkInsert(MediaContract.Albums.CONTENT_URI, albumValuesBatch);
// Iterate on each album, collect the artists and the genres and insert them
ContentValues albumArtistsValuesBatch[] = new ContentValues[artistsCount];
ContentValues albumGenresValuesBatch[] = new ContentValues[genresCount];
int artistCount = 0, genreCount = 0;
for (AudioType.DetailsAlbum album : albumList) {
for (int artistId : album.artistid) {
albumArtistsValuesBatch[artistCount] = new ContentValues();
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.HOST_ID, hostId);
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ALBUMID, album.albumid);
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ARTISTID, artistId);
artistCount++;
}
for (int genreId : album.genreid) {
albumGenresValuesBatch[genreCount] = new ContentValues();
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.HOST_ID, hostId);
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.ALBUMID, album.albumid);
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.GENREID, genreId);
genreCount++;
}
}
contentResolver.bulkInsert(MediaContract.AlbumArtists.CONTENT_URI, albumArtistsValuesBatch);
contentResolver.bulkInsert(MediaContract.AlbumGenres.CONTENT_URI, albumGenresValuesBatch);
}
private static void insertSongs(Context context, int hostId) throws ApiException, IOException {
AudioLibrary.GetSongs getSongs = new AudioLibrary.GetSongs();
ArrayList<AudioType.DetailsSong> songList = (ArrayList) getSongs.resultFromJson(Utils.readFile(context, "AudioLibrary.GetSongs.json")).items;
ContentValues songValuesBatch[] = new ContentValues[songList.size()];
for (int i = 0; i < songList.size(); i++) {
AudioType.DetailsSong song = songList.get(i);
songValuesBatch[i] = SyncUtils.contentValuesFromSong(hostId, song);
}
context.getContentResolver().bulkInsert(MediaContract.Songs.CONTENT_URI, songValuesBatch);
}
}

View File

@ -0,0 +1,155 @@
/*
* 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.support.test.espresso.Espresso;
import android.support.test.espresso.NoMatchingViewException;
import android.widget.AutoCompleteTextView;
import org.xbmc.kore.R;
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static android.support.test.espresso.action.ViewActions.clearText;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
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.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.containsString;
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());
}
}
public static void clickHomeButton() {
onView(withId(android.R.id.home)).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));
Espresso.closeSoftKeyboard();
}
/**
* 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());
Espresso.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
*/
public static void clickAdapterViewItem(int position, int resourceId) {
onData(anything()).inAdapterView(allOf(withId(resourceId), isDisplayed()))
.atPosition(position).perform(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
*/
public static void checkListMatchesSearchQuery(String query, int listSize, int resourceId) {
onView(allOf(withId(resourceId), isDisplayed()))
.check(matches(Matchers.withOnlyMatchingDataItems(Matchers.withItemContent(containsString(query)))));
onView(allOf(withId(resourceId), isDisplayed()))
.check(matches(Matchers.withAdapterSize(listSize)));
}
/**
* Checks if search action view does not exist in the current view hierarchy
*/
public static void checkSearchMenuCollapsed() {
onView(isAssignableFrom(AutoCompleteTextView.class)).check(doesNotExist());
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.support.test.espresso.IdlingResource;
import android.support.v4.app.LoaderManager;
public class LoaderIdlingResource implements IdlingResource {
private ResourceCallback mResourceCallback;
private LoaderManager loaderManager;
public LoaderIdlingResource(LoaderManager loaderManager) {
this.loaderManager = loaderManager;
}
@Override
public String getName() {
return LoaderIdlingResource.class.getName();
}
@Override
public boolean isIdleNow() {
boolean idle = !loaderManager.hasRunningLoaders();
if (idle && mResourceCallback != null) {
mResourceCallback.onTransitionToIdle();
}
return idle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
mResourceCallback = resourceCallback;
}
}

View File

@ -0,0 +1,131 @@
/*
* 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.database.Cursor;
import android.support.test.espresso.matcher.BoundedMatcher;
import android.support.test.espresso.matcher.CursorMatchers;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.AdapterView;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
public class Matchers {
public static MenuItemTitleMatcher withMenuTitle(String title) {
return new MenuItemTitleMatcher(title);
}
public static class MenuItemTitleMatcher extends BaseMatcher<Object> {
private final String title;
public MenuItemTitleMatcher(String title) { this.title = title; }
@Override
public boolean matches(Object o) {
if (o instanceof MenuItem) {
return ((MenuItem) o).getTitle().equals(title);
}
return false;
}
@Override
public void describeTo(Description description) { }
}
public static Matcher<View> withListSize(final int size) {
return new TypeSafeMatcher<View>() {
@Override public boolean matchesSafely(final View view) {
if (!(view instanceof ViewGroup))
return false;
return ((ViewGroup) view).getChildCount() == size;
}
@Override public void describeTo(final Description description) {
description.appendText("List should have " + size + " item(s)");
}
};
}
public static Matcher<View> withAdapterSize(final int size) {
return new TypeSafeMatcher<View>() {
@Override
protected boolean matchesSafely(View view) {
if (!(view instanceof AdapterView))
return false;
return ((AdapterView) view).getCount() == size;
}
@Override
public void describeTo(Description description) {
description.appendText("Adapter should have " + size + " item(s)");
}
};
}
public static Matcher<View> withOnlyMatchingDataItems(final Matcher<Object> dataMatcher) {
return new TypeSafeMatcher<View>() {
@Override
protected boolean matchesSafely(View view) {
if (!(view instanceof AdapterView))
return false;
Adapter adapter = ((AdapterView) view).getAdapter();
for (int i = 0; i < adapter.getCount(); i++) {
if (! dataMatcher.matches(adapter.getItem(i))) {
return false;
}
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("withOnlyMatchingDataItems: ");
dataMatcher.describeTo(description);
}
};
}
public static Matcher<Object> withItemContent(final Matcher<String> textMatcher) {
return new BoundedMatcher<Object, Cursor>(Cursor.class) {
@Override
protected boolean matchesSafely(Cursor item) {
for (int i = 0; i < item.getColumnCount();i++) {
switch (item.getType(i)) {
case Cursor.FIELD_TYPE_STRING:
if (CursorMatchers.withRowString(i, textMatcher).matches(item))
return true;
break;
}
}
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("withItemContent: ");
textMatcher.describeTo(description);
}
};
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.content.Context;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.support.test.rule.ActivityTestRule;
import android.support.v4.widget.DrawerLayout;
import android.util.Log;
import org.xbmc.kore.R;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.utils.LogUtils;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
public class Utils {
private static final String TAG = LogUtils.makeLogTag(Utils.class);
private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE";
private static final float DISABLED = 0.0f;
private static final float DEFAULT = 1.0f;
private static boolean isInitialized;
private static HostInfo hostInfo;
private static Context context;
public static String readFile(Context context, String filename) throws IOException {
InputStream is = context.getAssets().open(filename);
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
return new String(buffer, "UTF-8");
}
public static void closeDrawer(final ActivityTestRule<?> activityTestRule) throws Throwable {
activityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
DrawerLayout drawerLayout = (DrawerLayout) activityTestRule.getActivity().findViewById(R.id.drawer_layout);
drawerLayout.closeDrawers();
}
});
}
public static void initialize(ActivityTestRule<?> activityTestRule) throws Throwable {
if (isInitialized)
return;
context = activityTestRule.getActivity();
disableAnimations();
hostInfo = Database.fill(context);
HostManager.getInstance(context).switchHost(hostInfo);
Utils.closeDrawer(activityTestRule);
isInitialized = true;
}
public static void cleanup() {
Database.flush(context, hostInfo);
enableAnimations();
isInitialized = false;
}
private static void disableAnimations() {
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
if (permStatus == PackageManager.PERMISSION_GRANTED) {
setSystemAnimationsScale(DISABLED);
}
}
private static void enableAnimations() {
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
if (permStatus == PackageManager.PERMISSION_GRANTED) {
setSystemAnimationsScale(DEFAULT);
}
}
private static void setSystemAnimationsScale(float animationScale) {
try {
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
for (int i = 0; i < currentScales.length; i++) {
currentScales[i] = animationScale;
}
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
} catch (Exception e) {
Log.e("SystemAnimations", "Could not change animation scale to " + animationScale + " :'(");
}
}
}

View File

@ -0,0 +1,231 @@
/*
* 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.tests.ui;
import android.support.test.espresso.Espresso;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.testhelpers.LoaderIdlingResource;
import org.xbmc.kore.testhelpers.Utils;
import org.xbmc.kore.ui.MoviesActivity;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class RestoreSearchQueryListFragmentTest {
private final String SEARCH_QUERY = "Room";
private final int SEARCH_QUERY_LIST_SIZE = 2;
private final int COMPLETE_LIST_SIZE = 300;
private LoaderIdlingResource loaderIdlingResource;
@Rule
public ActivityTestRule<MoviesActivity> mActivityRule = new ActivityTestRule<>(
MoviesActivity.class);
@Before
public void setUp() throws Throwable {
Utils.initialize(mActivityRule);
loaderIdlingResource = new LoaderIdlingResource(mActivityRule.getActivity().getSupportLoaderManager());
Espresso.registerIdlingResources(loaderIdlingResource);
Utils.closeDrawer(mActivityRule);
}
@After
public void tearDown() throws Exception {
Espresso.unregisterIdlingResources(loaderIdlingResource);
}
@AfterClass
public static void cleanup() {
Utils.cleanup();
}
/**
* Simple test that checks if search query results in expected item(s)
*/
@Test
public void simpleSearchTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
EspressoTestUtils.checkTextInSearchQuery(SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(SEARCH_QUERY, SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Simple test that checks if search query is restored after device rotate
*/
@Test
public void simpleRotateTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
EspressoTestUtils.checkTextInSearchQuery(SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(SEARCH_QUERY, SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Test if search query is restored when user returns to list fragment from
* detail fragment
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Click on list item
* 3. Press back
* 4. Result: search query entered at 1. should be restored in search field
*/
@Test
public void searchClickBackTest() {
EspressoTestUtils.clearSearchQuery(mActivityRule.getActivity());
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
Espresso.pressBack();
EspressoTestUtils.checkTextInSearchQuery(SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(SEARCH_QUERY, SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Test if search query is restored when user returns to list fragment from
* detail fragment when device is rotated while on detail fragment
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Click on list item
* 3. Rotate device
* 4. Press back
* 5. Result: search query entered at 1. should be restored in search field
*/
@Test
public void searchClickRotateBackTest() {
EspressoTestUtils.clearSearchQuery(mActivityRule.getActivity());
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
Espresso.pressBack();
EspressoTestUtils.checkTextInSearchQuery(SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(SEARCH_QUERY, SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Test if saved search query is cleared when user clears the
* search query view
*
* UI interaction flow tested
* 1. Enter search query
* 2. Click on list item
* 3. Return to list
* 4. Clear search query
* 5. Click on list item
* 6. Return to list
* 7. Result: search query should be empty and collapsed
*/
@Test
public void searchClickBackClearSearchClickBackTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
Espresso.pressBack();
EspressoTestUtils.clearSearchQuery(mActivityRule.getActivity());
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
Espresso.pressBack();
EspressoTestUtils.checkSearchMenuCollapsed();
}
/**
* Test if after restoring search query the search query is cleared
* when user presses back again.
*
* UI interaction flow tested
* 1. Enter search query
* 2. Click on list item
* 3. Return to list
* 4. Press back
* 7. Result: search query should be cleared, collapsed, and list should show everything
*/
@Test
public void searchClickBackBackTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
Espresso.pressBack();
Espresso.pressBack();
EspressoTestUtils.checkSearchMenuCollapsed();
EspressoTestUtils.checkListMatchesSearchQuery("", COMPLETE_LIST_SIZE, R.id.list);
}
/**
* Test if pressing back clears a previous search
*
* UI interaction flow tested
* 1. Enter search query
* 2. Press back
* 3. Result: search query should be cleared, collapsed, and list should show everything
*/
public void searchBackTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
Espresso.pressBack();
EspressoTestUtils.checkSearchMenuCollapsed();
EspressoTestUtils.checkListMatchesSearchQuery("", COMPLETE_LIST_SIZE, R.id.list);
}
/**
* Test if after restoring the search query pressing home button up clears a previous search
*
* UI interaction flow tested
* 1. Enter search query
* 2. Click on list item
* 3. Press back
* 4. Press home button
* 5. Result: search query should be cleared, collapsed, and list should show everything
*/
public void searchClickBackUpTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
Espresso.pressBack();
EspressoTestUtils.clickHomeButton();
EspressoTestUtils.checkSearchMenuCollapsed();
EspressoTestUtils.checkListMatchesSearchQuery("", COMPLETE_LIST_SIZE, R.id.list);
}
/**
* Test if pressing home button up clears a previous search
*
* UI interaction flow tested
* 1. Enter search query
* 2. Press home button
* 3. Result: search query should be cleared, collapsed, and list should show everything
*/
public void searchUpTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
Espresso.pressBack();
EspressoTestUtils.clickHomeButton();
EspressoTestUtils.checkSearchMenuCollapsed();
EspressoTestUtils.checkListMatchesSearchQuery("", COMPLETE_LIST_SIZE, R.id.list);
}
}

View File

@ -0,0 +1,338 @@
/*
* 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.tests.ui;
import android.app.Activity;
import android.support.test.espresso.Espresso;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xbmc.kore.R;
import org.xbmc.kore.testhelpers.EspressoTestUtils;
import org.xbmc.kore.testhelpers.LoaderIdlingResource;
import org.xbmc.kore.testhelpers.Utils;
import org.xbmc.kore.ui.MusicActivity;
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.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class RestoreSearchQueryViewPagerTest {
private final String ARTIST_SEARCH_QUERY = "Ben";
private final int ARTIST_SEARCH_QUERY_LIST_SIZE = 2;
private final String ALBUMS_SEARCH_QUERY = "tes";
private final int ALBUM_SEARCH_QUERY_LIST_SIZE = 3;
private final int ARTIST_COMPLETE_LIST_SIZE = 224;
private final int ALBUM_COMPLETE_LIST_SIZE = 231;
private LoaderIdlingResource loaderIdlingResource;
@Rule
public ActivityTestRule<MusicActivity> mActivityRule = new ActivityTestRule<>(
MusicActivity.class);
@Before
public void setUp() throws Throwable {
Utils.initialize(mActivityRule);
loaderIdlingResource = new LoaderIdlingResource(mActivityRule.getActivity().getSupportLoaderManager());
Espresso.registerIdlingResources(loaderIdlingResource);
Utils.closeDrawer(mActivityRule);
}
@After
public void tearDown() throws Exception {
Espresso.unregisterIdlingResources(loaderIdlingResource);
}
@AfterClass
public static void cleanup() {
Utils.cleanup();
}
/**
* Simple test that checks if search query results in expected item(s)
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Result: search query entered at 1. should show in search field and list should match search query
*/
@Test
public void simpleSearchTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Simple test that checks if search query is restored after device rotate
* UI interaction flow tested:
* 1. Enter search query
* 2. Rotate device
* 3. Result: search query entered at 1. should show in search field and list should match search query
*/
@Test
public void simpleRotateTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Test if search query is restored when user returns to list fragment from
* detail fragment
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Click on list item
* 3. Press back
* 4. Result: search query entered at 1. should be restored in search field
*/
@Test
public void searchClickBackTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
Espresso.pressBack();
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Test if search query is restored when user returns to list fragment from
* detail fragment when device is rotated while on detail fragment
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Click on list item
* 3. Rotate device
* 4. Press back
* 5. Result: search query entered at 1. should be restored in search field
*/
@Test
public void searchClickRotateBackTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
EspressoTestUtils.clickAdapterViewItem(0, R.id.list);
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
Espresso.pressBack();
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Test if search query is cleared when switching to
* different tab in the TabAdapter
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Switch to Albums tab
* 3. Result: search query should be cleared
*/
@Test
public void searchSwitchTabTest() {
Activity activity = mActivityRule.getActivity();
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
clickAlbumsTab();
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
EspressoTestUtils.checkTextInSearchQuery("");
}
/**
* Tests if search query is still cleared when
* device is rotated after switching to a different tab
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Switch to Albums tab
* 3. Rotate device
* 4. Open search menu item
* 5. Result: search query should be cleared
*/
@Test
public void searchSwitchTabRotateTest() {
Activity activity = mActivityRule.getActivity();
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
clickAlbumsTab();
EspressoTestUtils.rotateDevice(activity);
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
EspressoTestUtils.checkTextInSearchQuery("");
EspressoTestUtils.checkListMatchesSearchQuery("", ALBUM_COMPLETE_LIST_SIZE, R.id.list);
}
/**
* Tests if search query is restored when returning
* to the original tab
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Switch to Albums tab
* 3. Switch to Artists tab
* 4. Result: search query entered at 1. should show in search field and list should match search query
*/
@Test
public void searchSwitchTabReturnTest() {
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
clickAlbumsTab();
clickArtistsTab();
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Tests if search query is restored when returning
* to the original tab after switching to a different
* tab and rotating the device
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Switch to Albums tab
* 3. Rotated device
* 4. Switch to Artists tab
* 5. Result: search query entered at 1. should show in search field and list should match search query
*/
@Test
public void searchSwitchTabRotateReturnTest() {
Activity activity = mActivityRule.getActivity();
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
clickAlbumsTab();
EspressoTestUtils.rotateDevice(activity);
clickArtistsTab();
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
}
/**
* Tests if search query is still cleared when user clears a previous
* search query and switches to a different tab and returns to the
* original tab
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Clear search query
* 3. Switch to Albums tab
* 4. Switch to Artists tab
* 5. Click search menu item
* 6. Result: search query should be cleared and list should contain all items
*/
@Test
public void searchClearSwitchTabSwitchBack() {
Activity activity = mActivityRule.getActivity();
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
EspressoTestUtils.clearSearchQuery(activity);
clickAlbumsTab();
clickArtistsTab();
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
EspressoTestUtils.checkTextInSearchQuery("");
EspressoTestUtils.checkListMatchesSearchQuery("", ARTIST_COMPLETE_LIST_SIZE, R.id.list);
}
/**
* Same test as {@link #searchClearSwitchTabSwitchBack()} but this time clearing performed using X button
*
* UI interaction flow tested:
* 1. Enter search query
* 2. Clear search query
* 3. Switch to Albums tab
* 4. Switch to Artists tab
* 5. Click search menu item using X button
* 6. Result: search query should be cleared and list should contain all items
*/
@Test
public void searchSwitchTabSwitchBackClearUsingXButtonSwitchTabSwitchBack() {
Activity activity = mActivityRule.getActivity();
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
clickAlbumsTab();
clickArtistsTab();
EspressoTestUtils.clearSearchQueryXButton(activity);
clickAlbumsTab();
clickArtistsTab();
EspressoTestUtils.checkSearchMenuCollapsed();
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
EspressoTestUtils.checkTextInSearchQuery("");
EspressoTestUtils.checkListMatchesSearchQuery("", ARTIST_COMPLETE_LIST_SIZE, R.id.list);
}
/**
* Tests if search queries for separate tabs are restored correctly
*
* UI interaction flow tested:
* 1. Enter search query artists tab
* 2. Enter search query albums tab
* 3. Switch to Artists tab
* 4. Result: search query entered at 1. should show in search field and list should match search query
* 5. Switch to Albums tab
* 6. Result: search query entered at 2. should show in search field and list should match search query
*/
@Test
public void searchArtistsSearchAlbumsSwitchArtists() {
Activity activity = mActivityRule.getActivity();
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
clickAlbumsTab();
EspressoTestUtils.enterSearchQuery(activity, ALBUMS_SEARCH_QUERY);
clickArtistsTab();
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
clickAlbumsTab();
EspressoTestUtils.checkTextInSearchQuery(ALBUMS_SEARCH_QUERY);
EspressoTestUtils.checkListMatchesSearchQuery(ALBUMS_SEARCH_QUERY, ALBUM_SEARCH_QUERY_LIST_SIZE, R.id.list);
}
private void clickAlbumsTab() {
onView(withId(R.id.pager_tab_strip)).perform(swipeLeft());
onView(withText(R.string.albums)).perform(click());
}
private void clickArtistsTab() {
onView(withId(R.id.pager_tab_strip)).perform(swipeRight());
onView(withText(R.string.artists)).perform(click());
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="org.xbmc.kore">
<!-- For espresso testing purposes, this is removed in live builds, but not in dev builds -->
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE" />
</manifest>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,247 @@
{
"id"