Commit 3ec8d068 authored by Shivansh Saini's avatar Shivansh Saini

UI test: Done FileBrowser and FilePicker

Signed-off-by: Shivansh Saini's avatarShivansh Saini <shivanshs9@gmail.com>
parent c2d6c12d
package org.videolan.vlc
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
import org.videolan.medialibrary.interfaces.media.AbstractMediaWrapper
import org.videolan.medialibrary.media.MediaLibraryItem
import org.videolan.vlc.gui.DiffUtilAdapter
abstract class DiffAdapterMatcher<D> : TypeSafeMatcher<D>()
fun withMediaType(mediaType: Int): DiffAdapterMatcher<MediaLibraryItem> {
return object : DiffAdapterMatcher<MediaLibraryItem>() {
override fun describeTo(description: Description) {
description.appendText("with media type: $mediaType")
}
override fun matchesSafely(item: MediaLibraryItem?): Boolean {
return (item as? AbstractMediaWrapper)?.type == mediaType
}
}
}
@ExperimentalCoroutinesApi
@ObsoleteCoroutinesApi
fun <D, VH : RecyclerView.ViewHolder> findFirstPosition(adapter: DiffUtilAdapter<D, VH>?, vararg matchers: DiffAdapterMatcher<D>): Int = adapter?.let {
val iter = it.dataset.iterator().withIndex()
while (iter.hasNext()) {
val index = iter.next()
if (matchers.all { it.matches(index.value) })
return index.index
}
return -1
} ?: -1
package org.videolan.vlc
import android.content.pm.ActivityInfo
import androidx.test.espresso.ViewAction
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import android.view.ViewGroup
import androidx.test.espresso.UiController
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import org.hamcrest.Matcher
/**
* The MIT License (MIT)
*
* Copyright (c) 2015 - Nathan Barraille
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*
* An Espresso ViewAction that changes the orientation of the screen
*/
class OrientationChangeAction internal constructor(private val orientation: Int) : ViewAction {
override fun getConstraints(): Matcher<View> {
return isRoot()
}
override fun getDescription(): String {
return "change orientation to $orientation"
}
override fun perform(uiController: UiController, view: View) {
uiController.loopMainThreadUntilIdle()
var activity = getActivity(view.context)
if (activity == null && view is ViewGroup) {
val c = view.childCount
var i = 0
while (i < c && activity == null) {
activity = getActivity(view.getChildAt(i).context)
++i
}
}
activity!!.requestedOrientation = orientation
val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
if (resumedActivities.isEmpty()) {
throw RuntimeException("Could not change orientation")
}
}
private fun getActivity(context: Context): Activity? {
var context = context
while (context is ContextWrapper) {
if (context is Activity) {
return context
}
context = (context as ContextWrapper).baseContext
}
return null
}
}
fun orientationLandscape(): ViewAction {
return OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
}
fun orientationPortrait(): ViewAction {
return OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
}
......@@ -5,12 +5,20 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.ColorInt
import androidx.annotation.IdRes
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.internal.util.Checks
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.videolan.medialibrary.interfaces.media.AbstractMediaWrapper
import org.videolan.vlc.gui.browser.BaseBrowserAdapter
import org.videolan.vlc.gui.helpers.SelectorViewHolder
import org.hamcrest.BaseMatcher
class RecyclerViewMatcher(@IdRes private val recyclerViewId: Int) {
......@@ -51,14 +59,10 @@ class RecyclerViewMatcher(@IdRes private val recyclerViewId: Int) {
fun withRecyclerView(@IdRes recyclerViewId: Int) = RecyclerViewMatcher(recyclerViewId)
fun withBgColor(@ColorInt color: Int): Matcher<View> {
Checks.checkNotNull(color)
return object : BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
public override fun matchesSafely(vg: ViewGroup): Boolean {
val colorDrawable = vg.background as ColorDrawable
println("v- ${colorDrawable.color}")
println("e - $color")
return color == colorDrawable.color
}
......@@ -66,4 +70,96 @@ fun withBgColor(@ColorInt color: Int): Matcher<View> {
description.appendText("with background color: $color")
}
}
}
\ No newline at end of file
}
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
class MediaRecyclerViewMatcher<VH : SelectorViewHolder<out ViewDataBinding>>(@IdRes private val recyclerViewId: Int) {
var recyclerView: RecyclerView? = null
fun atGivenType(mediaType: Int, matcher: Matcher<View>? = null): Matcher<View> {
return object : TypeSafeMatcher<View>() {
val mapVH: MutableMap<View, VH> = HashMap()
override fun describeTo(description: Description) {
description.appendText("with given media type: $mediaType")
}
override fun matchesSafely(view: View): Boolean {
if (!fillMatchesIfRequired(mapVH, view.rootView) { vh ->
if (vh is BaseBrowserAdapter.MediaViewHolder) {
val item = (vh as BaseBrowserAdapter.MediaViewHolder).binding.item as? AbstractMediaWrapper
item?.type == mediaType
} else false
}) return false
return mapVH[view]?.let {
scrollToShowItem(it.adapterPosition)
matcher?.matches(view) ?: true
} ?: false
}
}
}
private fun fillMatchesIfRequired(mapVH: MutableMap<View, VH>, rootView: View, condition: ((VH) -> Boolean)): Boolean {
if (recyclerView == null || mapVH.isEmpty()) {
recyclerView = rootView.findViewById(recyclerViewId) as RecyclerView
if (recyclerView!!.id == recyclerViewId) {
val it = (0 until recyclerView!!.adapter!!.itemCount).iterator()
while (it.hasNext()) {
val pos = it.nextInt()
val vh = try {
recyclerView!!.findViewHolderForAdapterPosition(pos) as VH
} catch (e: ClassCastException) {
null
} ?: continue
if (condition(vh)) {
mapVH[vh.itemView] = vh
}
}
} else return false
}
return true
}
fun scrollToShowItem(position: Int): Boolean {
recyclerView?.scrollToPosition(position)
return true
}
}
class FirstViewMatcher : BaseMatcher<View>() {
var matchedBefore = false
override fun matches(view: Any): Boolean = !matchedBefore.also { matchedBefore = true }
override fun describeTo(description: Description) {
description.appendText(" is the first view that comes along ")
}
}
fun firstView() = FirstViewMatcher()
fun sizeOfAtLeast(minSize: Int): Matcher<in View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("Recycler view has atleast $minSize items")
}
override fun matchesSafely(view: View): Boolean {
return view is RecyclerView && (view.adapter?.itemCount ?: 0) >= minSize
}
}
}
fun withCount(matcher: Matcher<Int>): Matcher<in View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("Recycler view has count with ${matcher.describeTo(description)}")
}
override fun matchesSafely(view: View): Boolean {
return view is RecyclerView && matcher.matches(view.adapter?.itemCount ?: 0)
}
}
}
......@@ -2,11 +2,14 @@ package org.videolan.vlc.gui.browser
import android.content.Intent
import android.view.View
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.*
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.*
import androidx.test.espresso.matcher.RootMatchers.*
import androidx.test.espresso.contrib.RecyclerViewActions.*
import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
......@@ -18,10 +21,14 @@ import org.hamcrest.TypeSafeMatcher
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.videolan.medialibrary.interfaces.media.AbstractMediaWrapper
import org.videolan.medialibrary.media.MediaLibraryItem
import org.videolan.vlc.*
import org.videolan.vlc.gui.DiffUtilAdapter
import org.videolan.vlc.gui.MainActivity
import org.videolan.vlc.gui.helpers.SelectorViewHolder
import org.videolan.vlc.util.EXTRA_TARGET
import java.lang.Thread.sleep
import org.videolan.vlc.util.Settings
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
......@@ -80,9 +87,9 @@ class FileBrowserFragmentUITest : BaseUITest() {
onView(withRecyclerView(R.id.network_list).atPosition(1)).perform(click())
onView(withId(R.id.main_toolbar))
.check(matches(
hasDescendant(withText(R.string.internal_memory))
))
.check(matches(
hasDescendant(withText(R.string.internal_memory))
))
}
@Test
......@@ -121,6 +128,8 @@ class FileBrowserFragmentUITest : BaseUITest() {
fun whenAtInternalStorage_checkSortMethods() {
onView(withRecyclerView(R.id.network_list).atPosition(1)).perform(click())
onView(isRoot()).perform(orientationPortrait())
openActionBarOverflowOrOptionsMenu(context)
onView(anyOf(withText(R.string.sortby), withId(R.id.ml_menu_sortby)))
......@@ -134,6 +143,19 @@ class FileBrowserFragmentUITest : BaseUITest() {
onView(anyOf(withText(R.string.sortby_filename), withId(R.id.ml_menu_sortby_filename)))
.inRoot(isPlatformPopup())
.check(matches(isDisplayed()))
onView(isRoot()).perform(orientationLandscape())
onView(anyOf(withContentDescription(R.string.sortby), withId(R.id.ml_menu_sortby)))
.check(matches(isDisplayed()))
.perform(click())
onView(anyOf(withText(R.string.sortby_name), withId(R.id.ml_menu_sortby_name)))
.inRoot(isPlatformPopup())
.check(matches(isDisplayed()))
onView(anyOf(withText(R.string.sortby_filename), withId(R.id.ml_menu_sortby_filename)))
.inRoot(isPlatformPopup())
.check(matches(isDisplayed()))
}
@Test
......@@ -189,15 +211,163 @@ class FileBrowserFragmentUITest : BaseUITest() {
.check(matches(sizeOfAtLeast(oldCount + 1)))
}
private fun sizeOfAtLeast(minSize: Int): Matcher<in View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("Recycler view has atleast $minSize items")
}
@Test
fun whenAtInternalStorage_checkMultipleSelectionWorks() {
onView(withRecyclerView(R.id.network_list).atPosition(1)).perform(click())
val rvMatcher = withRecyclerView(R.id.network_list)
onView(rvMatcher.atPosition(0)).perform(longClick())
onView(rvMatcher.atPosition(2)).perform(longClick())
onView(rvMatcher.atPosition(0)).check(matches(withBgColor(context.getColor(R.color.orange200transparent))))
onView(rvMatcher.atPosition(2)).check(matches(withBgColor(context.getColor(R.color.orange200transparent))))
}
@Test
fun whenAtInternalStorageAndFolderLongPress_checkAppbar() {
onView(withRecyclerView(R.id.network_list).atPosition(1)).perform(click())
override fun matchesSafely(view: View): Boolean {
return view is RecyclerView && (view.adapter?.itemCount ?: 0) >= minSize
}
val rvMatcher = MediaRecyclerViewMatcher<SelectorViewHolder<ViewDataBinding>>(R.id.network_list)
onView(allOf(
rvMatcher.atGivenType(AbstractMediaWrapper.TYPE_DIR), firstView()
)).perform(longClick())
onView(withId(R.id.action_mode_file_play))
.check(matches(isDisplayed()))
onView(withId(R.id.action_mode_file_delete))
.check(matches(isDisplayed()))
onView(withId(R.id.action_mode_file_add_playlist))
.check(matches(isDisplayed()))
}
@Test
fun whenAtBrowserFolderClickMore_checkContextMenu() {
onView(withRecyclerView(R.id.network_list).atPosition(1)).perform(click())
val rvMatcher = MediaRecyclerViewMatcher<SelectorViewHolder<ViewDataBinding>>(R.id.network_list)
onView(allOf(
withId(R.id.item_more), isDescendantOfA(rvMatcher.atGivenType(AbstractMediaWrapper.TYPE_DIR)), firstView()
)).perform(click())
assertThat(activity.supportFragmentManager.findFragmentByTag("context"), notNullValue())
onView(withId(R.id.ctx_list))
.check(matches(isDisplayed()))
.check(matches(sizeOfAtLeast(3)))
.check(matches(hasDescendant(withText(R.string.play))))
.check(matches(hasDescendant(withText(R.string.favorites_add))))
.check(matches(hasDescendant(withText(R.string.delete))))
}
@Test
fun whenAtMovieFolderAndVideoLongPress_checkAppbar() {
onView(allOf(
MediaRecyclerViewMatcher<SelectorViewHolder<ViewDataBinding>>(R.id.network_list).atGivenType(AbstractMediaWrapper.TYPE_DIR), hasDescendant(withText(containsString("Video")))
)).perform(click())
onView(allOf(
withId(R.id.item_more), isDescendantOfA(MediaRecyclerViewMatcher<SelectorViewHolder<ViewDataBinding>>(R.id.network_list).atGivenType(AbstractMediaWrapper.TYPE_DIR)), firstView()
)).perform(click())
onView(withId(R.id.action_mode_file_info))
.check(matches(isDisplayed()))
onView(withId(R.id.action_mode_file_play))
.check(matches(isDisplayed()))
onView(withId(R.id.action_mode_file_delete))
.check(matches(isDisplayed()))
onView(withId(R.id.action_mode_file_add_playlist))
.check(matches(isDisplayed()))
}
@Test
fun whenAtMovieFolderAndVideoClickMore_checkContextMenu() {
onView(allOf(
MediaRecyclerViewMatcher<SelectorViewHolder<ViewDataBinding>>(R.id.network_list).atGivenType(AbstractMediaWrapper.TYPE_DIR), hasDescendant(withText(containsString("Video")))
)).perform(click())
onView(allOf(
withId(R.id.item_more), isDescendantOfA(MediaRecyclerViewMatcher<SelectorViewHolder<ViewDataBinding>>(R.id.network_list).atGivenType(AbstractMediaWrapper.TYPE_VIDEO)), firstView()
)).perform(click())
assertThat(activity.supportFragmentManager.findFragmentByTag("context"), notNullValue())
onView(withId(R.id.ctx_list))
.check(matches(isDisplayed()))
.check(matches(sizeOfAtLeast(7)))
.check(matches(hasDescendant(withText(R.string.play_all))))
.check(matches(hasDescendant(withText(R.string.play_as_audio))))
.check(matches(hasDescendant(withText(R.string.append))))
.check(matches(hasDescendant(withText(R.string.info))))
.check(matches(hasDescendant(withText(R.string.download_subtitles))))
.check(matches(hasDescendant(withText(R.string.add_to_playlist))))
.check(matches(hasDescendant(withText(R.string.delete))))
}
@Test
fun whenAtInternalStorageAndContainsUnknownFile_checkShownOnlyIfSettingIsTrue() {
onView(withRecyclerView(R.id.network_list).atPosition(1)).perform(click())
val showAllFiles = Settings.getInstance(context).getBoolean("browser_show_all_files", true)
val rvMatcher = withRecyclerView(R.id.network_list)
onView(rvMatcher.atPosition(0))
.check(matches(isDisplayed()))
val adapter = rvMatcher.recyclerView?.adapter as? DiffUtilAdapter<MediaLibraryItem, RecyclerView.ViewHolder>
val pos = findFirstPosition(adapter, withMediaType(AbstractMediaWrapper.TYPE_ALL))
if (showAllFiles) {
assertThat(pos, not(equalTo(-1)))
onView(withId(R.id.network_list))
.perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(pos, longClick()))
assertThat(activity.supportFragmentManager.findFragmentByTag("context"), notNullValue())
} else {
assertThat(pos, equalTo(-1))
}
}
@Test
fun whenAtMusicFolderAndAudioClickMore_checkContextMenu() {
onView(allOf(
MediaRecyclerViewMatcher<SelectorViewHolder<ViewDataBinding>>(R.id.network_list).atGivenType(AbstractMediaWrapper.TYPE_DIR), hasDescendant(withText(containsString("Music")))
)).perform(click())
onView(allOf(
withId(R.id.item_more), isDescendantOfA(MediaRecyclerViewMatcher<SelectorViewHolder<ViewDataBinding>>(R.id.network_list).atGivenType(AbstractMediaWrapper.TYPE_AUDIO)), firstView()
)).perform(click())
assertThat(activity.supportFragmentManager.findFragmentByTag("context"), notNullValue())
onView(withId(R.id.ctx_list))
.check(matches(isDisplayed()))
.check(matches(sizeOfAtLeast(4)))
.check(matches(hasDescendant(withText(R.string.play_all))))
.check(matches(hasDescendant(withText(R.string.append))))
.check(matches(hasDescendant(withText(R.string.info))))
.check(matches(hasDescendant(withText(R.string.add_to_playlist))))
.check(matches(hasDescendant(withText(R.string.delete))))
}
@Test
fun whenAtRootAndClickedOnItemIcon_multiSelectionModeIsToggled() {
val rvMatcher = withRecyclerView(R.id.network_list)
onView(allOf(
withId(R.id.item_icon), isDescendantOfA(rvMatcher.atPosition(1))
)).perform(click())
onView(rvMatcher.atPosition(3)).perform(click())
onView(rvMatcher.atPosition(1)).check(matches(withBgColor(context.getColor(R.color.orange200transparent))))
onView(rvMatcher.atPosition(3)).check(matches(withBgColor(context.getColor(R.color.orange200transparent))))
onView(rvMatcher.atPosition(3)).perform(click())
onView(rvMatcher.atPosition(1)).perform(click())
onView(rvMatcher.atPosition(1)).check(matches(not(withBgColor(context.getColor(R.color.orange200transparent)))))
onView(rvMatcher.atPosition(3)).check(matches(not(withBgColor(context.getColor(R.color.orange200transparent)))))
}
}
\ No newline at end of file
package org.videolan.vlc.gui.browser
import androidx.test.rule.ActivityTestRule
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.*
import androidx.test.espresso.matcher.ViewMatchers.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.hamcrest.Matchers.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.videolan.vlc.BaseUITest
import org.videolan.vlc.R
import org.videolan.vlc.*
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
class FilePickerFragmentUITest : BaseUITest() {
@Rule
@JvmField
val activityTestRule = ActivityTestRule(FilePickerActivity::class.java)
lateinit var activity: FilePickerActivity
@Before
fun init() {
activity = activityTestRule.activity
}
@Test
fun whenAtSomeFolder_clickOnHomeIconReturnsBackToRoot() {
onView(withRecyclerView(R.id.network_list).atPosition(0)).perform(click())
onView(withRecyclerView(R.id.network_list).atPosition(0)).perform(click())
onView(withId(R.id.network_list)).check(matches(withCount(greaterThan(2))))
onView(withId(R.id.button_home)).perform(click())
onView(withId(R.id.network_list)).check(matches(withCount(equalTo(1))))
}
}
......@@ -42,6 +42,7 @@
android:orderInCategory="2"
android:title="@string/sortby"
android:icon="@drawable/ic_menu_sort"
android:contentDescription="@string/sortby"
android:id="@+id/ml_menu_sortby"
vlc:showAsAction="ifRoom|collapseActionView"
android:visible="false">
......
......@@ -12,7 +12,7 @@ import kotlinx.coroutines.channels.actor
abstract class DiffUtilAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>(), CoroutineScope {
override val coroutineContext = Dispatchers.Main.immediate
protected var dataset: List<D> = listOf()
var dataset: List<D> = listOf()
private set
private val diffCallback by lazy(LazyThreadSafetyMode.NONE) { createCB() }
private val updateActor = actor<List<D>>(capacity = Channel.CONFLATED) {
......
......@@ -71,8 +71,7 @@ class AudioBrowserAdapter @JvmOverloads constructor(
internal var cardSize: Int = SHOW_IN_LIST
) : PagedListAdapter<MediaLibraryItem,
AudioBrowserAdapter.AbstractMediaItemViewHolder<ViewDataBinding>>(DIFF_CALLBACK),
FastScroller.SeparatedAdapter, MultiSelectAdapter<MediaLibraryItem>, SwipeDragHelperAdapter
{
FastScroller.SeparatedAdapter, MultiSelectAdapter<MediaLibraryItem>, SwipeDragHelperAdapter {
val multiSelectHelper: MultiSelectHelper<MediaLibraryItem> = MultiSelectHelper(this, UPDATE_SELECTION)
private val defaultCover: BitmapDrawable?
private var focusNext = -1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment