Commit cea836bd authored by Shivansh Saini's avatar Shivansh Saini

UI test: Preference screens

- Last element in list cannot be scrolled to
- Advanced Preferences can't be tested due to above-mentioned bug
Signed-off-by: Shivansh Saini's avatarShivansh Saini <shivanshs9@gmail.com>
parent f03229e2
package org.videolan.vlc
import org.hamcrest.Matchers.`is`
import android.content.res.Resources
import androidx.preference.Preference
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
/** A collection of hamcrest matchers that match [Preference]s.
* These match with the [androidx.preference.Preference] class from androidX APIs.
**/
object PreferenceMatchers {
val isEnabled: Matcher<Preference>
get() = object : TypeSafeMatcher<Preference>() {
override fun describeTo(description: Description) {
description.appendText(" is an enabled preference")
}
public override fun matchesSafely(pref: Preference): Boolean {
return pref.isEnabled
}
}
fun withSummary(resourceId: Int): Matcher<Preference> {
return object : TypeSafeMatcher<Preference>() {
private var resourceName: String? = null
private var expectedText: String? = null
override fun describeTo(description: Description) {
description.appendText(" with summary string from resource id: ")
description.appendValue(resourceId)
if (null != resourceName) {
description.appendText("[")
description.appendText(resourceName)
description.appendText("]")
}
if (null != expectedText) {
description.appendText(" value: ")
description.appendText(expectedText)
}
}
public override fun matchesSafely(preference: Preference): Boolean {
if (null == expectedText) {
try {
expectedText = preference.context.resources.getString(resourceId)
resourceName = preference.context.resources.getResourceEntryName(resourceId)
} catch (ignored: Resources.NotFoundException) {
/* view could be from a context unaware of the resource id. */
}
}
return if (null != expectedText) {
expectedText == preference.summary.toString()
} else {
false
}
}
}
}
fun withSummaryText(summary: String): Matcher<Preference> {
return withSummaryText(`is`(summary))
}
fun withSummaryText(summaryMatcher: Matcher<String>): Matcher<Preference> {
return object : TypeSafeMatcher<Preference>() {
override fun describeTo(description: Description) {
description.appendText(" a preference with summary matching: ")
summaryMatcher.describeTo(description)
}
public override fun matchesSafely(pref: Preference): Boolean {
val summary = pref.summary.toString()
return summaryMatcher.matches(summary)
}
}
}
fun withTitle(resourceId: Int): Matcher<Preference> {
return object : TypeSafeMatcher<Preference>() {
private var resourceName: String? = null
private var expectedText: String? = null
override fun describeTo(description: Description) {
description.appendText(" with title string from resource id: ")
description.appendValue(resourceId)
if (null != resourceName) {
description.appendText("[")
description.appendText(resourceName)
description.appendText("]")
}
if (null != expectedText) {
description.appendText(" value: ")
description.appendText(expectedText)
}
}
public override fun matchesSafely(preference: Preference): Boolean {
if (null == expectedText) {
try {
expectedText = preference.context.resources.getString(resourceId)
resourceName = preference.context.resources.getResourceEntryName(resourceId)
} catch (ignored: Resources.NotFoundException) {
/* view could be from a context unaware of the resource id. */
}
}
return if (null != expectedText && preference.title != null) {
expectedText == preference.title.toString()
} else {
false
}
}
}
}
fun withTitleText(title: String): Matcher<Preference> {
return withTitleText(`is`(title))
}
fun withTitleText(titleMatcher: Matcher<String>): Matcher<Preference> {
return object : TypeSafeMatcher<Preference>() {
override fun describeTo(description: Description) {
description.appendText(" a preference with title matching: ")
titleMatcher.describeTo(description)
}
public override fun matchesSafely(pref: Preference): Boolean {
if (pref.title == null) {
return false
}
val title = pref.title.toString()
return titleMatcher.matches(title)
}
}
}
fun withKey(key: String): Matcher<Preference> {
return withKey(`is`(key))
}
fun withKey(keyMatcher: Matcher<String>): Matcher<Preference> {
return object : TypeSafeMatcher<Preference>() {
override fun describeTo(description: Description) {
description.appendText(" preference with key matching: ")
keyMatcher.describeTo(description)
}
public override fun matchesSafely(pref: Preference): Boolean {
return keyMatcher.matches(pref.key)
}
}
}
}
package org.videolan.vlc
import androidx.annotation.IdRes
import androidx.paging.PagedListAdapter
import androidx.preference.Preference
import androidx.preference.PreferenceGroupAdapter
import androidx.preference.PreferenceViewHolder
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
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.medialibrary.media.MediaLibraryItem
import org.videolan.vlc.gui.DiffUtilAdapter
import kotlin.math.min
abstract class DiffAdapterMatcher<D> : TypeSafeMatcher<D>()
......@@ -60,3 +72,24 @@ fun <D, VH : RecyclerView.ViewHolder> findFirstPosition(adapter: PagedListAdapte
}
return -1
} ?: -1
fun findFirstPreferencePosition(@IdRes recyclerViewId: Int, vararg matchers: Matcher<Preference>): Pair<Int, Int> {
val rvMatcher = withRecyclerView(recyclerViewId)
onView(rvMatcher.atPosition(0)).check(matches(isDisplayed()))
val adapter = rvMatcher.recyclerView?.adapter as PreferenceGroupAdapter
val count = adapter.itemCount
for (i in 0..count) {
if (matchers.all { it.matches(adapter.getItem(i)) }) return i to count
}
return -1 to count
}
fun onPreferenceRow(@IdRes recyclerViewId: Int, vararg matchers: Matcher<Preference>): ViewInteraction? = findFirstPreferencePosition(recyclerViewId, *matchers).let {(i, count) ->
if (i != -1) {
// FIXME: Fails to scroll to the bottom of recycler view
onView(withId(recyclerViewId))
.perform(RecyclerViewActions.scrollToPosition<PreferenceViewHolder>(min(i + 1, count - 1)))
onView(withRecyclerView(recyclerViewId).atPosition(i))
} else null
}
......@@ -2,11 +2,18 @@ package org.videolan.vlc
import android.content.Context
import android.content.res.Resources
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.StateListDrawable
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.matcher.BoundedMatcher
......@@ -20,14 +27,6 @@ import org.videolan.medialibrary.interfaces.media.AbstractMediaWrapper
import org.videolan.vlc.gui.browser.BaseBrowserAdapter
import org.videolan.vlc.gui.helpers.SelectorViewHolder
import org.videolan.vlc.gui.helpers.ThreeStatesCheckbox
import android.graphics.drawable.BitmapDrawable
import android.graphics.Bitmap
import android.graphics.drawable.StateListDrawable
import android.graphics.drawable.Drawable
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.appcompat.view.menu.ActionMenuItemView
class RecyclerViewMatcher(@IdRes private val recyclerViewId: Int) {
var recyclerView: RecyclerView? = null
......@@ -39,6 +38,7 @@ class RecyclerViewMatcher(@IdRes private val recyclerViewId: Int) {
fun atPositionOnView(position: Int, targetViewId: Int): Matcher<View> {
return object : TypeSafeMatcher<View>() {
var childView: View? = null
var triedMatch = false
override fun describeTo(description: Description) {
description.appendText("Recycler view doesn't have item at position $position")
......@@ -53,13 +53,16 @@ class RecyclerViewMatcher(@IdRes private val recyclerViewId: Int) {
}
override fun matchesSafely(view: View): Boolean {
if (childView == null) {
recyclerView = view.rootView.findViewById(recyclerViewId) as RecyclerView
if (recyclerView!!.id == recyclerViewId) {
childView = recyclerView!!.findViewHolderForAdapterPosition(position)?.itemView
} else {
return false
}
if (!triedMatch && childView == null) {
if (recyclerView == null) recyclerView = view.rootView.findViewById(recyclerViewId) as RecyclerView
triedMatch = true
recyclerView?.run {
if (id == recyclerViewId) {
scrollToPosition(position + 1)
childView = findViewHolderForAdapterPosition(position)?.itemView
true
} else null
} ?: return false
}
return if (targetViewId == -1) {
......@@ -67,7 +70,6 @@ class RecyclerViewMatcher(@IdRes private val recyclerViewId: Int) {
} else {
view === childView?.findViewById<View>(targetViewId)
}
}
}
}
......@@ -264,3 +266,16 @@ fun withActionIconDrawable(@DrawableRes resourceId: Int): Matcher<View> {
}
}
}
fun withResName(resName: String): Matcher<View> {
return object: TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("with res-name: $resName")
}
override fun matchesSafely(view: View): Boolean {
val identifier = view.resources.getIdentifier(resName, "id", "android")
return resName.isNotEmpty() && (view.id == identifier)
}
}
}
package org.videolan.vlc.gui.preferences
import android.content.SharedPreferences
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatCheckedTextView
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf
import org.videolan.vlc.BaseUITest
import org.videolan.vlc.PreferenceMatchers
import org.videolan.vlc.R
import org.videolan.vlc.onPreferenceRow
import org.videolan.vlc.util.Settings
abstract class BasePreferenceUITest : BaseUITest() {
val settings: SharedPreferences = Settings.getInstance(context)
private fun checkListPreferenceValidSelected(@StringRes textRes: Int) {
Espresso.onView(ViewMatchers.withText(textRes))
.check(ViewAssertions.matches(ViewMatchers.isChecked()))
}
private fun checkListPreferenceValidSelected(text: String) {
Espresso.onView(ViewMatchers.withText(text))
.check(ViewAssertions.matches(ViewMatchers.isChecked()))
}
fun checkModeChanged(key: String, mode: String, defValue: String, map: Map<String, *>) {
val oldMode = settings.getString(key, defValue) ?: defValue
onPreferenceRow(R.id.recycler_view, PreferenceMatchers.withKey(key))!!
.perform(ViewActions.click())
val oldVal = map.getValue(oldMode).toString()
oldVal.toIntOrNull()?.let {
checkListPreferenceValidSelected(it)
} ?: checkListPreferenceValidSelected(oldVal)
val newVal = map.getValue(mode).toString()
val matcher = newVal.toIntOrNull()?.let {
ViewMatchers.withText(it)
} ?: ViewMatchers.withText(newVal)
Espresso.onView(allOf(ViewMatchers.isAssignableFrom(AppCompatCheckedTextView::class.java), matcher))
.perform(ViewActions.click())
ViewMatchers.assertThat(settings.getString(key, defValue), Matchers.`is`(mode))
}
fun checkToggleWorks(key: String, settings: SharedPreferences, default: Boolean = true) {
val oldValue = settings.getBoolean(key, default)
onPreferenceRow(R.id.recycler_view, PreferenceMatchers.withKey(key))!!
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(if (oldValue) ViewMatchers.isChecked() else ViewMatchers.isNotChecked())))
.perform(ViewActions.click())
val newValue = settings.getBoolean(key, true)
ViewMatchers.assertThat("'$key' setting didn't update", newValue, Matchers.not(Matchers.equalTo(oldValue)))
}
}
\ No newline at end of file
package org.videolan.vlc.gui.preferences
import android.os.Build
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.filters.SdkSuppress
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.junit.Rule
import org.junit.Test
import org.videolan.vlc.PreferenceMatchers.withKey
import org.videolan.vlc.R
import org.videolan.vlc.onPreferenceRow
import org.videolan.vlc.util.KEY_PLAYBACK_SPEED_PERSIST
import org.videolan.vlc.util.RESUME_PLAYBACK
@ExperimentalCoroutinesApi
@ObsoleteCoroutinesApi
class PreferencesAudioUiTest: BasePreferenceUITest() {
@get:Rule
val intentsTestRule = IntentsTestRule(PreferencesActivity::class.java)
lateinit var activity: PreferencesActivity
override fun beforeTest() {
activity = intentsTestRule.activity
onPreferenceRow(R.id.recycler_view, withKey("audio_category"))!!
.perform(click())
}
@Test
fun checkResumePlaybackAfterCallSetting() {
val key = RESUME_PLAYBACK
checkToggleWorks(key, settings)
}
@Test
fun checkSavePlaybackSpeedSetting() {
val key = KEY_PLAYBACK_SPEED_PERSIST
checkToggleWorks(key, settings)
}
@Test
fun checkPersistentAudioPlaybackSetting() {
val key = "audio_task_removed"
checkToggleWorks(key, settings, default = false)
}
@Test
fun checkDigitalAudioSetting() {
val key = "audio_digital_output"
checkToggleWorks(key, settings)
}
@Test
fun checkPersistAudioRepeatModeSetting() {
val key = "audio_save_repeat"
checkToggleWorks(key, settings, default = false)
}
@Test
fun checkDetectHeadsetSetting() {
val key = "enable_headset_detection"
checkToggleWorks(key, settings)
}
@Test
fun checkResumeOnHeadsetSetting() {
val key = "enable_play_on_headset_insertion"
checkToggleWorks(key, settings, default = false)
}
@Test
@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.O)
fun checkAudioDuckingSetting() {
val key = "audio_ducking"
checkToggleWorks(key, settings)
}
@Test
fun checkAudioOutputModeSetting() {
// TODO: Fails due to android bug in scrolling
val key = "aout"
checkModeChanged(key, "0", "0", MAP_AOUT)
checkModeChanged(key, "1", "0", MAP_AOUT)
}
companion object {
val MAP_AOUT = mapOf("0" to R.string.aout_audiotrack, "1" to R.string.aout_opensles)
}
}
\ No newline at end of file
package org.videolan.vlc.gui.preferences
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.hamcrest.Matchers.not
import org.junit.Rule
import org.junit.Test
import org.videolan.vlc.PreferenceMatchers.isEnabled
import org.videolan.vlc.PreferenceMatchers.withKey
import org.videolan.vlc.R
import org.videolan.vlc.onPreferenceRow
@ExperimentalCoroutinesApi
@ObsoleteCoroutinesApi
class PreferencesCastingUITest: BasePreferenceUITest() {
@get:Rule
val intentsTestRule = IntentsTestRule(PreferencesActivity::class.java)
lateinit var activity: PreferencesActivity
override fun beforeTest() {
activity = intentsTestRule.activity
onPreferenceRow(R.id.recycler_view, withKey("casting_category"))!!
.perform(click())
}
@Test
fun checkWirelessCastingSetting() {
val key = "enable_casting"
onPreferenceRow(R.id.recycler_view, withKey("casting_passthrough"), isEnabled)!!
.check(matches(isDisplayed()))
checkToggleWorks(key, settings)
onPreferenceRow(R.id.recycler_view, withKey("casting_passthrough"), not(isEnabled))!!
.check(matches(isDisplayed()))
}
@Test
fun checkAudioPassthroughSetting() {
val key = "casting_passthrough"
checkToggleWorks(key, settings, default = false)
}
@Test
fun checkCastingQualitySetting() {
val key = "casting_quality"
checkModeChanged(key, "0", "2", MAP_CASTING_QUALITY)
checkModeChanged(key, "1", "2", MAP_CASTING_QUALITY)
checkModeChanged(key, "2", "2", MAP_CASTING_QUALITY)
checkModeChanged(key, "3", "2", MAP_CASTING_QUALITY)
}
companion object {
val MAP_CASTING_QUALITY = mapOf(
"0" to R.string.casting_quality_high, "1" to R.string.casting_quality_medium, "2" to R.string.casting_quality_low,
"3" to R.string.casting_quality_lowcpu
)
}
}
\ No newline at end of file
package org.videolan.vlc.gui.preferences
import android.content.ComponentName
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.hamcrest.Matchers.allOf
import org.junit.Rule
import org.junit.Test
import org.videolan.vlc.PreferenceMatchers.withKey
import org.videolan.vlc.R
import org.videolan.vlc.gui.SecondaryActivity
import org.videolan.vlc.onPreferenceRow
import org.videolan.vlc.util.KEY_VIDEO_APP_SWITCH
import org.videolan.vlc.util.PLAYBACK_HISTORY
import org.videolan.vlc.util.SCREEN_ORIENTATION
@ExperimentalCoroutinesApi
@ObsoleteCoroutinesApi
class PreferencesFragmentUITest: BasePreferenceUITest() {
@get:Rule
val intentsTestRule = IntentsTestRule(PreferencesActivity::class.java)
lateinit var activity: PreferencesActivity
override fun beforeTest() {
activity = intentsTestRule.activity
}
@Test
fun clickOnMediaFolders_openDirectoriesActivity() {
val key = "directories"
onPreferenceRow(R.id.recycler_view, withKey(key))!!
.check(matches(isDisplayed()))
.perform(click())
intended(allOf(
hasComponent(ComponentName(context, SecondaryActivity::class.java)),
hasExtra("fragment", SecondaryActivity.STORAGE_BROWSER)
))
}
@Test
fun clickOnToggleRescan_keyToggled() {
val key = "auto_rescan"
checkToggleWorks(key, settings)
}
@Test
fun checkPipModeSetting() {
val key = KEY_VIDEO_APP_SWITCH
checkModeChanged(key, "0", "0", MAP_PIP_MODE)
checkModeChanged(key, "1", "0", MAP_PIP_MODE)
checkModeChanged(key, "2", "0", MAP_PIP_MODE)
}
@Test
fun checkHardwareAccelerationSetting() {
val key = "hardware_acceleration"
checkModeChanged(key, "-1", "-1", MAP_HARDWARE_ACCEL)
checkModeChanged(key, "0", "-1", MAP_HARDWARE_ACCEL)
checkModeChanged(key, "1", "-1", MAP_HARDWARE_ACCEL)
checkModeChanged(key, "2", "-1", MAP_HARDWARE_ACCEL)
}
@Test
fun checkScreenOrientationSetting() {
val key = SCREEN_ORIENTATION
checkModeChanged(key, "99", "99", MAP_ORIENTATION)
checkModeChanged(key, "100", "99", MAP_ORIENTATION)
checkModeChanged(key, "101", "99", MAP_ORIENTATION)
checkModeChanged(key, "102", "99", MAP_ORIENTATION)
}
@Test
fun checkPlaybackHistorySetting() {
val key = PLAYBACK_HISTORY
checkToggleWorks(key, settings)
}
companion object {
val MAP_PIP_MODE = mapOf("0" to R.string.stop, "1" to R.string.play_as_audio_background, "2" to R.string.play_pip_title)
val MAP_HARDWARE_ACCEL = mapOf("-1" to R.string.automatic, "0" to R.string.hardware_acceleration_disabled, "1" to R.string.hardware_acceleration_decoding, "2" to R.string.hardware_acceleration_full)
val MAP_ORIENTATION = mapOf("99" to R.string.screen_orientation_sensor, "100" to R.string.screen_orientation_start_lock, "101" to R.string.screen_orientation_landscape, "102" to R.string.screen_orientation_portrait)
}
}
\ No newline at end of file
package org.videolan.vlc.gui.preferences
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.intent.rule.IntentsTestRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.junit.Rule
import org.junit.Test
import org.videolan.vlc.PreferenceMatchers.withKey
import org.videolan.vlc.R
import org.videolan.vlc.onPreferenceRow
@ExperimentalCoroutinesApi
@ObsoleteCoroutinesApi
class PreferencesSubtitlesUITest: BasePreferenceUITest() {
@get:Rule
val intentsTestRule = IntentsTestRule(PreferencesActivity::class.java)
lateinit var activity: PreferencesActivity
override fun beforeTest() {
activity = intentsTestRule.activity
onPreferenceRow(R.id.recycler_view, withKey("subtitles_category"))!!
.perform(click())
}
@Test
fun checkAutoLoadSubtitleSetting() {
val key = "subtitles_autoload"
checkToggleWorks(key, settings)
}
@Test
fun checkSubtitleSizeSetting() {
val key = "subtitles_size"
checkModeChanged(key, "19", "16", MAP_SUBTITLE_SIZE)
checkModeChanged(key, "16", "16", MAP_SUBTITLE_SIZE)
checkModeChanged(key, "13", "16", MAP_SUBTITLE_SIZE)
checkModeChanged(key, "10", "16", MAP_SUBTITLE_SIZE)
}
@Test
fun checkSubtitleColorSetting() {
val key = "subtitles_color"
checkModeChanged(key, "65535", "16777215", MAP_SUBTITLE_COLOR)
checkModeChanged(key, "16776960", "16777215", MAP_SUBTITLE_COLOR)
checkModeChanged(key, "65280", "16777215", MAP_SUBTITLE_COLOR)
checkModeChanged(key, "16711935", "16777215", MAP_SUBTITLE_COLOR)
checkModeChanged(key, "12632256", "16777215", MAP_SUBTITLE_COLOR)
checkModeChanged(key, "16777215", "16777215", MAP_SUBTITLE_COLOR)
}
@Test