Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use alarm stream to play presets in Wake-up Timers #729

Merged
merged 6 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ dependencies {
implementation 'com.google.android.material:material:1.4.0'
implementation 'com.google.code.gson:gson:2.8.7'
implementation 'com.hopenlib.library:flextools:1.0.1'
implementation 'com.ncorti:slidetoact:0.9.0'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'org.greenrobot:eventbus:3.1.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
Expand Down
115 changes: 28 additions & 87 deletions app/src/androidTest/java/com/github/ashutoshgngwr/noice/EspressoX.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,23 @@ package com.github.ashutoshgngwr.noice

import android.content.Intent
import android.view.View
import android.widget.CompoundButton
import android.widget.TimePicker
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.MotionEvents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.util.TreeIterables
import com.github.ashutoshgngwr.noice.widget.DurationPicker
import com.google.android.material.slider.Slider
import com.google.android.material.textfield.TextInputLayout
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.*
import org.hamcrest.TypeSafeMatcher
import kotlin.reflect.KClass

/**
* [EspressoX] contains the custom extended util implementations for Espresso.
Expand Down Expand Up @@ -80,71 +75,6 @@ object EspressoX {
}
}

/**
* [withSliderValue] matches a [Slider] with the provided [expectedValue].
*/
fun withSliderValue(expectedValue: Float): Matcher<View> {
return object : BoundedMatcher<View, Slider>(Slider::class.java) {
override fun describeTo(description: Description) {
description.appendText("Slider with value $expectedValue")
}

override fun matchesSafely(slider: Slider): Boolean {
return expectedValue == slider.value
}
}
}

private fun searchForView(viewMatcher: Matcher<View>): ViewAction {
return object : ViewAction {
override fun getConstraints() = isRoot()
override fun getDescription() = "search for view with $viewMatcher in the root view"

override fun perform(uiController: UiController, view: View) {
TreeIterables.breadthFirstViewTraversal(view).forEach {
if (viewMatcher.matches(it)) {
return
}
}

throw NoMatchingViewException.Builder()
.withRootView(view)
.withViewMatcher(viewMatcher)
.build()
}
}
}

/**
* [waitForView] tries to find a view with given [viewMatchers]. If found, it returns the
* [ViewInteraction] for given [viewMatchers]. If not found, it waits for given [wait]
* before attempting to find the view again. It reties for given number of [retries].
*
* Adaptation of the [StackOverflow post by manbradcalf](https://stackoverflow.com/a/56499223/2410641)
*/
fun waitForView(
vararg viewMatchers: Matcher<View>,
retries: Int = 5,
wait: Long = 1000L,
): ViewInteraction {
require(retries > 0 && wait > 0)
val viewMatcher = allOf(*viewMatchers)
for (i in 0 until retries) {
try {
onView(isRoot()).perform(searchForView(viewMatcher))
break
} catch (e: NoMatchingViewException) {
if (i == retries) {
throw e
}

Thread.sleep(wait)
}
}

return onView(viewMatcher)
}

/**
* Returns a [ViewAction] that invokes [DurationPicker.onDurationAddedListener] with the given
* [durationSecs].
Expand Down Expand Up @@ -194,22 +124,6 @@ object EspressoX {
}
}

/**
* [setChecked] returns a [ViewAction] to set the checked state of a [CompoundButton].
*/
fun setChecked(checked: Boolean): ViewAction {
return object : ViewAction {
override fun getDescription() = "check/uncheck compound buttons"
override fun getConstraints() = instanceOf<View>(CompoundButton::class.java)

override fun perform(uiController: UiController, view: View) {
if (view is CompoundButton) {
view.isChecked = checked
}
}
}
}

/**
* Returns a matcher that matches the nested intent sent with an Intent chooser.
*/
Expand All @@ -230,4 +144,31 @@ object EspressoX {
override fun perform(uiController: UiController, view: View) = Unit
}
}

/**
* Retries the given [block] by catching the [expectedErrors] until all [retries] are exhausted.
* It waits for a defined [wait] period between each retry.
*/
inline fun retryWithWaitOnError(
vararg expectedErrors: KClass<out Throwable>,
retries: Int = 15,
wait: Long = 500,
block: () -> Unit,
) {
require(retries > 0 && wait > 0)

var i = 0
while (i++ < retries) {
try {
block.invoke()
break
} catch (e: Throwable) {
if (!expectedErrors.any { it.isInstance(e) } || i == retries) {
throw e
}

Thread.sleep(wait)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
break
} catch (t: Throwable) {
if (i == retryCount) {
throw Exception("Test failed after $retryCount retries", t)
throw t
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.github.ashutoshgngwr.noice.activity

import android.content.Intent
import androidx.fragment.app.Fragment
import androidx.fragment.app.testing.FragmentScenario
import androidx.lifecycle.Lifecycle
import androidx.media.AudioAttributesCompat
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.ashutoshgngwr.noice.R
import com.github.ashutoshgngwr.noice.playback.PlaybackController
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class AlarmRingerActivityTest {

@Before
fun setup() {
mockkObject(PlaybackController)
every { PlaybackController.setAudioUsage(any(), any()) } returns Unit
every { PlaybackController.pause(any()) } returns Unit
every { PlaybackController.playPreset(any(), any()) } returns Unit
}

@After
fun teardown() {
unmockkAll()
}

@Test
fun testWithoutPresetID() {
val scenario = ActivityScenario.launch(AlarmRingerActivity::class.java)
assertEquals(Lifecycle.State.DESTROYED, scenario.state)
}

@Test
fun testWithPresetID() {
val presetID = "test-preset-id"

// cannot launch Activity using `ActivityScenario.launch(Intent)` method. For whatever reasons,
// it increases the startup time for all subsequent `ActivityScenario.launch(Class)`s from
// other classes.
FragmentScenario.launch(Fragment::class.java).onFragment {
it.startActivity(
Intent(it.requireContext(), AlarmRingerActivity::class.java)
.putExtra(AlarmRingerActivity.EXTRA_PRESET_ID, presetID)
)
}

verify(exactly = 1, timeout = 10000L) {
PlaybackController.setAudioUsage(any(), AudioAttributesCompat.USAGE_ALARM)
PlaybackController.playPreset(any(), presetID)
}

clearMocks(PlaybackController)
onView(withId(R.id.dismiss_slider)).perform(swipeRight())

verify(exactly = 1, timeout = 10000L) {
PlaybackController.setAudioUsage(any(), AudioAttributesCompat.USAGE_MEDIA)
PlaybackController.pause(any())
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.github.ashutoshgngwr.noice.activity

import androidx.core.content.edit
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.ashutoshgngwr.noice.EspressoX
import com.github.ashutoshgngwr.noice.RetryTestRule
import org.junit.After
import org.junit.Assert.assertEquals
Expand All @@ -33,20 +35,18 @@ class AppIntroActivityTest {

@After
fun teardown() {
activityScenario.close()
PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext())
.edit(commit = true) {
clear()
}
.edit { clear() }
}

@Test
fun testOnSkipPressed() {
activityScenario.onActivity {
it.onSkipPressed(null)
}

// should destroy the activity
assertTrue(it.isFinishing || it.isDestroyed)
EspressoX.retryWithWaitOnError(AssertionError::class) {
assertEquals(Lifecycle.State.DESTROYED, activityScenario.state)
}

// should update the preferences
Expand All @@ -60,9 +60,10 @@ class AppIntroActivityTest {
fun testOnDonePressed() {
activityScenario.onActivity {
it.onDonePressed(null)
}

// should destroy the activity
assertTrue(it.isFinishing || it.isDestroyed)
EspressoX.retryWithWaitOnError(AssertionError::class) {
assertEquals(Lifecycle.State.DESTROYED, activityScenario.state)
}

// should update the preferences
Expand Down Expand Up @@ -94,9 +95,7 @@ class AppIntroActivityTest {
// when user has already seen the activity once, i.e., if the preference is present in the
// storage, maybeStart shouldn't start the activity.
PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext())
.edit(commit = true) {
putBoolean(AppIntroActivity.PREF_HAS_USER_SEEN_APP_INTRO, true)
}
.edit { putBoolean(AppIntroActivity.PREF_HAS_USER_SEEN_APP_INTRO, true) }

activityScenario.onActivity {
AppIntroActivity.maybeStart(it)
Expand Down
Loading