diff --git a/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java b/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java index 93032a9..2e05e50 100644 --- a/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java +++ b/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java @@ -103,6 +103,18 @@ public class EspressoTestUtils { 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 @@ -201,6 +213,15 @@ public class EspressoTestUtils { .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 */ diff --git a/app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java b/app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java index 119be69..b6ddfb0 100644 --- a/app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java +++ b/app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java @@ -22,7 +22,7 @@ import android.support.test.espresso.matcher.CursorMatchers; import android.support.v7.widget.RecyclerView; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; +import android.widget.ListView; import android.widget.SeekBar; import android.widget.TextView; @@ -54,13 +54,11 @@ public class Matchers { public void describeTo(Description description) { } } - public static Matcher withListSize(final int size) { + public static Matcher withListViewSize(final int size) { return new TypeSafeMatcher() { @Override public boolean matchesSafely(final View view) { - if (!(view instanceof ViewGroup)) - return false; - - return ((ViewGroup) view).getChildCount() == size; + return (view instanceof ListView) && + ((ListView) view).getAdapter().getCount() == size; } @Override public void describeTo(final Description description) { diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java index a3bc81a..c070b14 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java @@ -87,9 +87,9 @@ public class SlideUpPanelTests extends AbstractTestClass { super.setUp(); getPlaylistHandler().reset(); - getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.AUDIO, createMusicItem(0, 0)); - getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createVideoItem(0, 1)); - getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createMusicVideoItem(0, 2)); + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.AUDIO, createMusicItem(0, 0), true); + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createVideoItem(0, 1), false); + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createMusicVideoItem(0, 2), false); getPlayerHandler().reset(); getPlayerHandler().setPlaylists(getPlaylistHandler().getPlaylists()); diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/playlistfragment/TCP/PlaylistTests.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/playlistfragment/TCP/PlaylistTests.java new file mode 100644 index 0000000..06e2625 --- /dev/null +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/playlistfragment/TCP/PlaylistTests.java @@ -0,0 +1,293 @@ +/* + * Copyright 2018 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.remote.playlistfragment.TCP; + +import android.content.Context; +import android.support.test.rule.ActivityTestRule; +import android.view.View; +import android.widget.TextView; + +import org.junit.Rule; +import org.junit.Test; +import org.xbmc.kore.R; +import org.xbmc.kore.testhelpers.EspressoTestUtils; +import org.xbmc.kore.testhelpers.action.ViewActions; +import org.xbmc.kore.tests.ui.AbstractTestClass; +import org.xbmc.kore.testutils.tcpserver.handlers.PlayerHandler; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Player; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Playlist; +import org.xbmc.kore.ui.sections.remote.RemoteActivity; + +import java.util.List; +import java.util.concurrent.TimeoutException; + +import static android.support.test.espresso.Espresso.onView; +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.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.isRoot; +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.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.xbmc.kore.testutils.TestUtils.createMusicItem; +import static org.xbmc.kore.testutils.TestUtils.createPictureItem; +import static org.xbmc.kore.testutils.TestUtils.createVideoItem; +import static org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Playlist.OnClear; + +public class PlaylistTests extends AbstractTestClass { + + private static final int PLAYLIST_SIZE = 10; + + @Rule + public ActivityTestRule remoteActivityActivityTestRule = + new ActivityTestRule<>(RemoteActivity.class); + + @Override + protected ActivityTestRule getActivityTestRule() { + return remoteActivityActivityTestRule; + } + + @Override + protected void setSharedPreferences(Context context) { + + } + + @Override + public void setUp() throws Throwable { + int itemId = 0; + + getPlaylistHandler().reset(); + for (int i = 0; i < PLAYLIST_SIZE; i++) { + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.AUDIO, createMusicItem(i, itemId++), false); + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createVideoItem(i, itemId++), false); + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.PICTURE, createPictureItem(i, itemId++), false); + } + + getPlayerHandler().reset(); + getPlayerHandler().setPlaylists(getPlaylistHandler().getPlaylists()); + getPlayerHandler().startPlay(Playlist.playlistID.AUDIO, 0); + + // Checking for available playlists is done in PlaylistFragment on startup + // and every 10 seconds. To make sure PlaylistFragment can get the available + // playlists at startup, the activity needs to be created after the backend + // has been fully setup. + super.setUp(); + + onView(isRoot()).perform(swipeLeft()); + waitForAudioPlaylistToShow(); + } + + /** + * Test if playlist is not cleared when playback is stopped + * + * UI interaction flow tested: + * 1. Start playing multiple music items + * 2. Stop playback + * 3. Result: playlist should still be visible + */ + @Test + public void keepPlaylistOnStop() { + onView(isRoot()).perform(swipeRight()); + EspressoTestUtils.clickButton(R.id.stop); + onView(isRoot()).perform(swipeLeft()); + + assertEquals(getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size(), PLAYLIST_SIZE); + EspressoTestUtils.checkListViewSize(PLAYLIST_SIZE, R.id.playlist); + } + + /** + * Test if playlist is not cleared when playback is paused + * + * UI interaction flow tested: + * 1. Start playing multiple music items + * 2. Pause playback + * 3. Result: playlist should still be visible + */ + @Test + public void keepPlaylistOnPause() { + onView(isRoot()).perform(swipeRight()); + EspressoTestUtils.clickButton(R.id.play); + onView(isRoot()).perform(swipeLeft()); + + assertEquals(getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size(), PLAYLIST_SIZE); + EspressoTestUtils.checkListViewSize(PLAYLIST_SIZE, R.id.playlist); + } + + /** + * Test if playlist is cleared when cleared on Kodi + * + * UI interaction flow tested: + * 1. Start playing multiple music items + * 2. Clear playlist on server (Kodi) + * 3. Result: playlist should be empty + */ + @Test + public void clearPlaylistWhenClearedOnKodi() throws Exception { + getPlaylistHandler().clearPlaylist(Playlist.playlistID.AUDIO); + getConnectionHandlerManager().waitForNotification(OnClear.METHOD_NAME, 10000); + + assertEquals(0, getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size()); + onView(allOf(withId(R.id.info_title), withText(R.string.playlist_empty))) + .check(matches(isDisplayed())); + } + + /** + * Test if playback of a playlist is resumed after stopping playback + * + * UI interaction flow tested: + * 1. Start playing multiple music items + * 2. Stop playback + * 3. Click on playlist item + * 4. Result: playback should resume from clicked playlist item + */ + @Test + public void stopPlayingAndResumeNextItem() throws TimeoutException { + int positionClicked = 3; + onView(isRoot()).perform(swipeRight()); + EspressoTestUtils.clickButton(R.id.stop); + onView(isRoot()).perform(swipeLeft()); + getConnectionHandlerManager().clearMethodsHandled(); + EspressoTestUtils.clickAdapterViewItem(positionClicked, R.id.playlist); + getConnectionHandlerManager().waitForMethodHandled(Player.Open.METHOD_NAME, 10000); + + List playlistOnServer = getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO); + assertSame(getPlayerHandler().getPlayState(), PlayerHandler.PLAY_STATE.PLAYING); + assertEquals("Playlist on server has size " + playlistOnServer.size() + + " but should be " + PLAYLIST_SIZE, playlistOnServer.size(), PLAYLIST_SIZE); + assertEquals("Current playing item ID is " + getPlayerHandler().getMediaItem().getLibraryId() + + ", but this should be " + playlistOnServer.get(positionClicked).getLibraryId(), + getPlayerHandler().getMediaItem().getLibraryId(), playlistOnServer.get(positionClicked).getLibraryId()); + } + + /** + * Test if playlist is correctly restored after playback has stopped + * and device configuration changed + * UI interaction flow tested: + * 1. Start playing multiple music items + * 2. Rotate device + * 3. Result: playlist should be the same as before rotation + */ + @Test + public void restorePlaylistAfterConfigurationChange() { + getConnectionHandlerManager().clearMethodsHandled(); + EspressoTestUtils.rotateDevice(getActivity()); + waitForAudioPlaylistToShow(); + + assertEquals(getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size(), PLAYLIST_SIZE); + EspressoTestUtils.checkListViewSize(PLAYLIST_SIZE, R.id.playlist); + } + + /** + * Test if playlist is correctly restored after playback has stopped + * and device configuration changed + * UI interaction flow tested: + * 1. Start playing multiple music items + * 2. Stop playback + * 3. Rotate device + * 4. Result: playlist should be the same as before rotation + */ + @Test + public void restorePlaylistAfterStopAndConfigurationChange() { + onView(isRoot()).perform(swipeRight()); + EspressoTestUtils.clickButton(R.id.stop); + onView(isRoot()).perform(swipeLeft()); + + getConnectionHandlerManager().clearMethodsHandled(); + EspressoTestUtils.rotateDevice(getActivity()); + waitForAudioPlaylistToShow(); + + assertEquals(getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size(), PLAYLIST_SIZE); + EspressoTestUtils.checkListViewSize(PLAYLIST_SIZE, R.id.playlist); + } + + /** + * Test if playlist for currently playing item is shown even if other + * playlists are available on server + * UI interaction flow tested: + * 1. Add audio and video playlists on server + * 2. Start playing video item + * 3. Result: playlist for video items should be shown + */ + @Test + public void showCurrentlyPlayingPlaylist() { + getPlayerHandler().startPlay(Playlist.playlistID.VIDEO, 0); + onView(isRoot()).perform(ViewActions.waitForView(R.id.playlist_item_title, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((TextView) v).getText().toString().contains("Video"); + } + }, 10000)); + + assertEquals("Playlist on server has size " + + getPlaylistHandler().getPlaylist(Playlist.playlistID.VIDEO).size() + + " but should be " + PLAYLIST_SIZE, + getPlaylistHandler().getPlaylist(Playlist.playlistID.VIDEO).size(), PLAYLIST_SIZE); + assertEquals("Got media type " + + getPlayerHandler().getMediaItem().getType() + + ", this should be " + Player.GetItem.TYPE.movie.name(), + getPlayerHandler().getMediaItem().getType(), Player.GetItem.TYPE.movie.name()); + + onView(allOf(withText(getPlayerHandler().getMediaItem().getTitle()), isDisplayed())).check(matches(isDisplayed())); + } + + /** + * Test if playlist for last played item is shown when playback has stopped + * and other playlists are available on server + * UI interaction flow tested: + * 1. Add audio, picture, and video playlists on server + * 2. Start playing video item + * 3. Stop playback + * 4. Result: playlist for video items should be shown + */ + @Test + public void showLastActivePlaylist() { + getPlayerHandler().startPlay(Playlist.playlistID.VIDEO, 0); + onView(isRoot()).perform(swipeRight()); + EspressoTestUtils.clickButton(R.id.stop); + + onView(isRoot()).perform(swipeLeft()); + onView(isRoot()).perform(ViewActions.waitForView(R.id.playlist_item_title, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((TextView) v).getText().toString().contains("Video"); + } + }, 10000)); + + assertEquals("Playlist on server has size " + + getPlaylistHandler().getPlaylist(Playlist.playlistID.VIDEO).size() + + " but should be " + PLAYLIST_SIZE, + getPlaylistHandler().getPlaylist(Playlist.playlistID.VIDEO).size(), PLAYLIST_SIZE); + assertEquals("Got media type " + + getPlayerHandler().getMediaItem().getType() + + ", this should be " + Player.GetItem.TYPE.movie.name(), + getPlayerHandler().getMediaItem().getType(), Player.GetItem.TYPE.movie.name()); + + onView(allOf(withText(getPlayerHandler().getMediaItem().getTitle()), isDisplayed())).check(matches(isDisplayed())); + } + + private void waitForAudioPlaylistToShow() { + onView(isRoot()).perform(ViewActions.waitForView(R.id.playlist_item_title, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return "Music 1".contentEquals(((TextView) v).getText()); + } + }, 10000)); + } +} diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java index cb4e031..1fa94ee 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java @@ -71,7 +71,7 @@ public class PlayerHandler extends ConnectionHandler { this.elapsedTime = 0; this.playState = STOPPED; playerType = PlayerType.GetActivePlayersReturnType.AUDIO; - playlists.clear(); + playlists = null; setMediaType(Player.GetItem.TYPE.unknown); } @@ -160,7 +160,7 @@ public class PlayerHandler extends ConnectionHandler { * Starts playing current item in the playlist */ public void startPlay() { - if (playlists.size() > 0 && activePlaylistId != null) { + if (playlists != null && playlists.size() > 0 && activePlaylistId != null) { mediaItem = playlists.get(activePlaylistId.ordinal()).getCurrentItem(); if (mediaItem != null) { @@ -178,6 +178,8 @@ public class PlayerHandler extends ConnectionHandler { } public void startPlay(Playlist.playlistID playlistId, int playlistPosition) { + if (playlists == null) return; + activePlaylistId = playlistId; PlaylistHolder playlistHolder = playlists.get(playlistId.ordinal()); @@ -275,7 +277,7 @@ public class PlayerHandler extends ConnectionHandler { } private JsonResponse handleGetItem(int methodId) { - if (playlists.size() > 0) { + if (playlists != null && playlists.size() > 0) { mediaItem = playlists.get(activePlaylistId.ordinal()).getCurrentItem(); } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHandler.java index 3beb723..879690c 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHandler.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHandler.java @@ -111,7 +111,7 @@ public class PlaylistHandler extends ConnectionHandler { playlists.get(playlistId).clear(); } - public void addItemToPlaylist(Playlist.playlistID id, Player.GetItem item) { + public void addItemToPlaylist(Playlist.playlistID id, Player.GetItem item, boolean sentNotification) { int playlistId = id.ordinal(); while (playlists.size() <= playlistId) { @@ -125,7 +125,9 @@ public class PlaylistHandler extends ConnectionHandler { } playlist.add(item); - OnAdd onAddNotification = new OnAdd(item.getLibraryId(), item.getType(), playlistId, playlist.getIndexOf(item)); - addNotification(onAddNotification); + if (sentNotification) { + OnAdd onAddNotification = new OnAdd(item.getLibraryId(), item.getType(), playlistId, playlist.getIndexOf(item)); + addNotification(onAddNotification); + } } }