Dzinek
Dzinek

Reputation: 806

Espresso-Intents - Matchers and stubbing

I have a Send Feedback in my app's Preferences. It should open a Chooser and a new Intent, to send an email with provided email address, subject and text template. This part simply works both on emulator and physical device:

prefButtonSendFeedback?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
            val emailIntent = Intent(Intent.ACTION_SENDTO)
            emailIntent.type = "text/plain"
            emailIntent.data = Uri.parse("mailto:")
            emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.send_feedback_email_address)))
            emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.send_feedback_email_subject))
            emailIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.send_feedback_email_content_template))
            context?.startActivity(Intent.createChooser(emailIntent, getString(R.string.send_feedback_intent_name)))
            true
        }

I have also Espresso test for this case:

    @Test fun clickOnFeedbackActionMenuItem_StartsFeedbackFragment() {
        launchFragmentInContainer<SettingsFragment>()

        Intents.init()

        intending(
            hasAction(Intent.ACTION_CHOOSER)
        ).respondWith(
            ActivityResult(Activity.RESULT_OK, null)
        )

        // Press on Feedback navigation item
        onView(withId(androidx.preference.R.id.recycler_view))
            .perform(
                actionOnItem<RecyclerView.ViewHolder>(
                    hasDescendant(withText(R.string.pref_send_feedback)), click()
                )
            )

        val emailIntent = allOf(
            hasAction(Intent.ACTION_SENDTO),
            hasData(Uri.parse("mailto:")),
            hasExtra(Intent.EXTRA_EMAIL, arrayOf(withText(R.string.send_feedback_email_address))),
            hasExtra(Intent.EXTRA_SUBJECT, withText(R.string.send_feedback_email_subject)),
            hasExtra(Intent.EXTRA_TEXT, withText(R.string.send_feedback_email_content_template))
        )

        val expectedIntent = allOf(
            hasAction(Intent.ACTION_CHOOSER),
            hasExtra(equalTo(Intent.EXTRA_INTENT), emailIntent),
            hasExtra(equalTo(Intent.EXTRA_TITLE), withText(R.string.send_feedback_intent_name))
        )

        intended(expectedIntent)

        Intents.release()
    }

My issue is similar in general to one from How to stub Intent.createChooser Intent using Espresso

androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: Wanted to match 1 intents. Actually matched 0 intents.

IntentMatcher: (has action: is "android.intent.action.CHOOSER" and has extras: has bundle with: key: "android.intent.extra.INTENT" value: (has action: is "android.intent.action.SENDTO" and has data: is <mailto:> and has extras: has bundle with: key: is "android.intent.extra.EMAIL" value: is [<with string from resource id: <3134850762>>] and has extras: has bundle with: key: is "android.intent.extra.SUBJECT" value: is <with string from resource id: <2131820702>> and has extras: has bundle with: key: is "android.intent.extra.TEXT" value: is <with string from resource id: <2131820701>>))

Matched intents:[]

Recorded intents:
-Intent { act=android.intent.action.CHOOSER (has extras) } handling packages:[[android]], extras:[Bundle[{android.intent.extra.INTENT=Intent { act=android.intent.action.SENDTO dat=mailto: (has extras) }, android.intent.extra.TITLE=Send Feedback:}]])

        val emailIntent = allOf(
            hasAction(Intent.ACTION_SENDTO),
            hasData(Uri.parse("mailto:")),
        )

        val expectedIntent = allOf(
            hasAction(Intent.ACTION_CHOOSER),
            hasExtra(equalTo(Intent.EXTRA_INTENT), emailIntent),
        )
        val receivedIntent: Intent = Iterables.getOnlyElement(Intents.getIntents())
        assertThat(receivedIntent).hasAction(Intent.ACTION_CHOOSER) ### here is a breakpoint

It shows me, that my Intent has all Extras I need (subject, address and text) - I removed unnecessary values:

receivedIntent =
  (...)
  mAction = "android.intent.action.CHOOSER"
  (..)
  mExtras = 
    mMap = 
      value[0] = 
        mAction = "android.intent.action.SENDTO"
        mData = "mailto:"
        mExtras = 
          mMap =
            value[0] = "App Feedback"
            value[1] = "Add here any bugs, issues or ideas about App"
            value[2] =
              0 = "[email protected]"
      value[1] = "Send Feedback:"

My question is, what is wrong with my Matchers - for me they look ok:

Moreover, I am not sure how intending() works - I assume that using it like in my code will not run real Intent during test. So it will not run an email client. I am wrong - it runs, but without it all next test hangs even, if I am using Intents.release().

Upvotes: 3

Views: 1354

Answers (3)

dominicoder
dominicoder

Reputation: 10175

I still don't understand the difference, so if anyone can explain it to me...

withText is a Matcher<View> which is for matching TextViews - it's specifically looking for a TextView that has the specified string resource ID. Unless you're shoving a TextView with the same resource ID into your extras, it ain't going to work.

Upvotes: 0

brice
brice

Reputation: 1

hasExtra(key: String, value: T)
-> hasEntry(is(key), is(value))

hasExtra(keyMatcher, valueMatcher)
-> BundleMatcher(keyMatcher, valueMatcher)

hasExtras(matcher: Mathcer)
-> new TypeSafeMatcher
-> matcher.matches(intent.getExtras())

I use like this simply.

intended(FilterMatcher.filter<Intent> { intent ->
    //check with intent  
})

class FilterMatcher<T> (klass: Class<T>, val filter: (data: T) -> Boolean): TypeSafeMatcher<T>(klass) {
    companion object {
        inline fun <reified T> filter(noinline filter: (data: T) -> Boolean): Matcher<T> = FilterMatcher(T::class.java, filter) as Matcher<T>
        inline fun <reified T: View> filterView(noinline filter: (data: T) -> Boolean): Matcher<View> = FilterMatcher(T::class.java, filter) as Matcher<View>
    }

    override fun describeTo(description: Description?) {
        description?.appendText("with filter");
    }

    override fun matchesSafely(item: T): Boolean = filter(item)
}

Upvotes: 0

Dzinek
Dzinek

Reputation: 806

So, once I dig deeper in web resources, I found more complex Espresso matchers:

        val expectedIntent = allOf(
            hasAction(Intent.ACTION_CHOOSER),
            hasExtra(
                equalTo(Intent.EXTRA_INTENT),
                allOf(
                    hasAction(Intent.ACTION_SENDTO),
                    hasData(Uri.parse("mailto:")),
                    hasExtra(
                        `is`(Intent.EXTRA_TEXT),
                        `is`("Add here any bugs, issues or ideas about App")
                    ),
                    hasExtra(
                        `is`(Intent.EXTRA_EMAIL),
                        `is`(arrayOf("[email protected]"))
                    ),
                    hasExtra(
                        `is`(Intent.EXTRA_SUBJECT),
                        `is`("App Feedback")
                    )
                )
            ),
            hasExtra(
                `is`(Intent.EXTRA_TITLE),
                `is`("Send Feedback:")
            )
        )

        intended(expectedIntent)

It simply works.

I still don't understand the difference, so if anyone can explain it to me...

I don't know, why this one works:

            hasExtra(
                `is`(Intent.EXTRA_TITLE),
                `is`("Send Feedback:")
            )

and this one not:

            hasExtra(
                `is`(Intent.EXTRA_TITLE),
                 withText(R.string.send_feedback_intent_name)
            )

Upvotes: 3

Related Questions