Use this skill to write Espresso 3.7.0 tests against Android Views — `onView(matcher).perform(action).check(matches(...))`, `onData(...)` for `AdapterView`, `RecyclerViewActions`, `Intents.intended/intending`, and `IdlingResource` registration. Covers the full `ViewMatchers` / `ViewActions` / `ViewAssertions` catalog, the `RootMatchers.DEFAULT` exclusion of dialogs and popups (the `inRoot(isDialog())` requirement), the `BottomNavigationViewActions` doesn't-exist gotcha in 3.7.0 contrib, and the `PickerActions.setDate(year, month, day)` 1-12 month convention. Use when the user reports `NoMatchingViewException`, `AmbiguousViewMatcherException`, `PerformException`, asks "how do I click in a dialog", "Espresso typeText doesn't fire", "intent stub for camera", or "Espresso vs UiAutomator".
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Use this skill to write Espresso 3.7.0 tests against Android Views — `onView(matcher).perform(action).check(matches(...))`, `onData(...)` for `AdapterView`, `RecyclerViewActions`, `Intents.intended/intending`, and `IdlingResource` registration. Covers the full `ViewMatchers` / `ViewActions` / `ViewAssertions` catalog, the `RootMatchers.DEFAULT` exclusion of dialogs and popups (the `inRoot(isDialog())` requirement), the `BottomNavigationViewActions` doesn't-exist gotcha in 3.7.0 contrib, and the `PickerActions.setDate(year, month, day)` 1-12 month convention. Use when the user reports `NoMatchingViewException`, `AmbiguousViewMatcherException`, `PerformException`, asks "how do I click in a dialog", "Espresso typeText doesn't fire", "intent stub for camera", or "Espresso vs UiAutomator".
license
Apache-2.0. See LICENSE for complete terms.
metadata
{"author":"Jaewoong Eum (skydoves)","keywords":["espresso","onView","ViewMatchers","ViewActions","ViewAssertions","RootMatchers","IdlingResource","IdlingPolicies","RecyclerViewActions","Intents","PickerActions","NoMatchingViewException"]}
Writing Espresso Tests — The Idle-Then-Act Model for Android Views
Espresso is the canonical instrumentation-time UI test framework for Android Views (not Compose — for Compose, use the compose/ skill set; for cross-Compose-View interop see ../../../compose/interop/testing-with-espresso-interop/SKILL.md). Each onView(...).perform(...).check(...) call internally pumps the main looper to idle, finds exactly one matching view, runs the action, and pumps to idle again before checking. This skill encodes the full matcher/action/assertion catalog, the RootMatchers.DEFAULT trap, and the per-version gotchas.
When to use this skill
The user is testing an Activity, Fragment, or Dialog whose UI is built with Android Views (XML layouts, AppCompat, Material View components).
A test fails with NoMatchingViewException, AmbiguousViewMatcherException, or PerformException.
A click on an AlertDialog button silently does nothing — RootMatchers.DEFAULT excludes dialogs.
The user wants to stub Intent.startActivityForResult for camera, contacts, or share flows.
The user wants to register an IdlingResource for a long-running task that Espresso can't see.
The user is debugging a RecyclerView interaction.
The user mentions BottomNavigationViewActions and is surprised it isn't on the classpath in Espresso 3.7.0 contrib.
When NOT to use this skill
The UI is Jetpack Compose — use compose/ skills (../../../compose/finders/finding-nodes-by-tag-text-content/SKILL.md, etc.). Mixed Compose+View hosts: ../../../compose/interop/testing-with-espresso-interop/SKILL.md.
The test crosses app boundaries (Settings, dialer, system UI) — InjectEventSecurityException from Espresso. Use ../../uiautomator/cross-app-tests-with-uiautomator/SKILL.md.
The runner stack is not yet set up — start with ../../runner/running-instrumented-tests-with-androidjunit4/SKILL.md.
The Activity has not been launched — pair with ../../scenarios/launching-activities-with-activityscenario/SKILL.md.
Prerequisites
Runner stack from ../../runner/running-instrumented-tests-with-androidjunit4/SKILL.md.
Espresso artifacts (3.7.0 GA per docs/CORPUS.md H.1):
dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.7.0") // RecyclerViewActions, PickerActions
androidTestImplementation("androidx.test.espresso:espresso-intents:3.7.0") // Intents.intended/intending
androidTestImplementation("androidx.test.espresso:espresso-accessibility:3.7.0")
androidTestImplementation("androidx.test.espresso:espresso-web:3.7.0") // WebView
implementation("androidx.test.espresso:espresso-idling-resource:3.7.0") // for production code that exposes IdlingResources
}
android.testOptions.animationsDisabled = true// hermetic
An Activity (or Fragment) launched via ActivityScenario / ActivityScenarioRule / FragmentScenario.
Workflow
1. Find views with onView(matcher). The matcher is a Hamcrest Matcher<View>. Identity-first matchers (most preferred): withId(R.id.button), withTagKey(int), withResourceName(String). Text/desc fallback: withText, withContentDescription, withHint. Source: androidx.test.espresso.matcher.ViewMatchers (lines 79-1000+, R3 lines 180-280):
matches(matcher) passes when the resolved view matches; doesNotExist() passes when the matcher resolved zero views (NOT when one was found and is invisible — use matches(not(isDisplayed())) for that). Source: R3 lines 290-311.
5. Recognize the implicit idle. Every onView(...).perform(...) and .check(...) call internally:
Submits the action on the main thread via the dagger-injected mainThreadExecutor.
Calls uiController.loopMainThreadUntilIdle() until: the main MessageQueue is empty, all IdlingResources registered with IdlingRegistry report isIdleNow == true, all registered non-main Loopers are idle, and the AsyncTask thread pool is idle.
Resolves the view (viewMatcher against the current Root's hierarchy). 0 → NoMatchingViewException; 2+ → AmbiguousViewMatcherException.
Checks viewAction.getConstraints().matches(view) → throws PerformException if false.
Loops the main thread to idle again before returning.
This is the idle-then-act model. Tests that work locally but fail on CI are usually missing an IdlingResource for a non-Looper-driven async path (custom ExecutorService, OkHttp's dispatcher, etc.). Source: R3 lines 165-178, 811-820.
6. For dialogs and popups, override the root with inRoot(...).RootMatchers.DEFAULT excludes dialogs, popups, and toasts (it matches "the focused activity window"). For an AlertDialog button:
RootMatchers.DEFAULT is allOf(hasWindowLayoutParams(), allOf(anyOf(allOf(isDialog(), withDecorView(hasWindowFocus())), isSubwindowOfCurrentActivity()), isFocusable())) per R3 line 311 — that's why a generic dialog button click silently misses.
7. For RecyclerView, use RecyclerViewActions from contrib.onView(...).perform(...) against a RecyclerView's row directly does NOT work because the row may be off-screen (no view to find):
IntentsTestRule is @Deprecated per docs/CORPUS.md H.7 — use IntentsRule or call Intents.init() / Intents.release() manually.
10. Register IdlingResource for non-Looper async work. Espresso watches the main looper and the AsyncTask pool by default; for OkHttp, custom ExecutorService, or sensor pipelines you must surface idleness:
classCountingIdlingResource(privateval name: String) : IdlingResource {
privateval counter = AtomicInteger(0)
privatevar callback: IdlingResource.ResourceCallback? = nulloverridefungetName() = name
overridefunisIdleNow() = counter.get() == 0overridefunregisterIdleTransitionCallback(cb: IdlingResource.ResourceCallback) { callback = cb }
funincrement() { counter.incrementAndGet() }
fundecrement() { if (counter.decrementAndGet() == 0) callback?.onTransitionToIdle() }
}
@BeforefunsetUp() = IdlingRegistry.getInstance().register(myIdler)
@AfterfuntearDown() = IdlingRegistry.getInstance().unregister(myIdler)
// Master policy default is 60 seconds; setIdlingResourceTimeout default is 26 seconds.// For debugger sessions, opt out of timeouts entirely with
IdlingPolicies.setMasterPolicyTimeout(, TimeUnit.SECONDS)
IdlingPolicies.setIdlingResourceTimeout(, TimeUnit.SECONDS)
Wrap network calls / async work in increment() / decrement() from production code (the :espresso-idling-resource artifact is on implementation, not androidTestImplementation).
11. Read errors precisely.
NoMatchingViewException — the matcher resolved zero views in the current root. Confirm the view is currently displayed; if it is in a dialog, add .inRoot(isDialog()).
AmbiguousViewMatcherException — 2+ views matched. Tighten with allOf(...).
PerformException — the action's constraint failed (e.g. click() requires isDisplayingAtLeast(90)). The exception message includes the constraint description.
InjectEventSecurityException — an event would cross to a foreign window. The test reached system UI. Use UiAutomator instead.
Patterns
Pattern: WRONG vs RIGHT — clicking a dialog button
// WRONG
onView(withText("Submit")).perform(click())
// WRONG because: RootMatchers.DEFAULT (line 51 of RootMatchers.java) excludes dialogs.// The matcher does not see the dialog window. Result: NoMatchingViewException ("No views in// hierarchy found matching: with text Submit"), even though the dialog is clearly on screen.
Same applies to popup menus (isPlatformPopup()) and toasts.
Pattern: WRONG vs RIGHT — BottomNavigationView interaction (Espresso 3.7.0 contrib)
// WRONGimport androidx.test.espresso.contrib.BottomNavigationViewActions
onView(withId(R.id.bottom_nav)).perform(BottomNavigationViewActions.selectItem(R.id.tab_profile))
// WRONG because: BottomNavigationViewActions does NOT exist in espresso-contrib 3.7.0.// The class was historically documented but never shipped in 3.7.0's contrib jar (R3 lines 91-93).// Compile fails with "unresolved reference".
// RIGHT — click the underlying menu item view directlyimport com.google.android.material.bottomnavigation.BottomNavigationView
import org.hamcrest.Matchers.allOf
onView(allOf(
withId(R.id.tab_profile),
isDescendantOfA(isAssignableFrom(BottomNavigationView::class.java)),
)).perform(click())
Pattern: WRONG vs RIGHT — PickerActions.setDate month convention
// WRONGimport androidx.test.espresso.contrib.PickerActions
onView(withClassName(equalTo(DatePicker::class.java.name)))
.perform(PickerActions.setDate(2026, 4, 15)) // intends "May 15, 2026"// WRONG because: PickerActions.setDate uses 1-12 month convention (it subtracts 1 internally// before calling DatePicker.updateDate which uses 0-11). Passing 4 sets April, not May.// Source: R3 line 460, PickerActions.java line 49.
// RIGHT — May = 5
onView(withClassName(equalTo(DatePicker::class.java.name)))
.perform(PickerActions.setDate(2026, 5, 15))
Pattern: WRONG vs RIGHT — Thread.sleep to wait for an animation
// WRONG
onView(withId(R.id.fab)).perform(click())
Thread.sleep(500) // wait for the FAB exit animation
onView(withId(R.id.fab)).check(doesNotExist())
// WRONG because: Thread.sleep is a smell. Espresso's idle pump already waits for animations// driven through the main looper; if it doesn't, register an IdlingResource for the offending// async path. See `docs/CORPUS.md` D.7 — "Thread.sleep is a smell".
// RIGHT — disable animations OR register an IdlingResource
android.testOptions.animationsDisabled = true// for property animators driven via Choreographer// And/or, for non-Looper waits:
IdlingPolicies.setMasterPolicyTimeout(45, TimeUnit.SECONDS)
IdlingRegistry.getInstance().register(myAnimationIdler)
Pattern: scrolling a RecyclerView to an off-screen row before clicking it
scrollTo and actionOnItem use a HOLDER matcher (matches the underlying RecyclerView.ViewHolder's itemView), not a Matcher<View> against the screen.
Mandatory rules
MUST add .inRoot(isDialog()) for any matcher targeting an AlertDialog content view, .inRoot(isPlatformPopup()) for popup menus and toasts. RootMatchers.DEFAULT excludes them.
MUST prefer withId / withTagKey / withResourceName over withText. Text matchers are i18n-fragile and churn with copy edits.
MUST NOT rely on Thread.sleep to wait for animations or async work. Use IdlingResource + IdlingPolicies.setMasterPolicyTimeout, and testOptions.animationsDisabled = true.
MUST NOT import androidx.test.espresso.contrib.BottomNavigationViewActions — the class is not in espresso-contrib:3.7.0. Use the isDescendantOfA(isAssignableFrom(BottomNavigationView::class.java)) workaround.
MUST pass 1-12 (not 0-11) for the month parameter of PickerActions.setDate(year, month, day). The action subtracts 1 internally before calling DatePicker.updateDate.
MUST call Intents.init() / Intents.release() (or use IntentsRule) — intended / intending only work between those calls.
MUST put :espresso-idling-resource:3.7.0 on implementation (production), not androidTestImplementation. The production code is what increments / decrements the resource.
MUST NOT use IntentsTestRule — it is @Deprecated. Use IntentsRule (3.5+) or manual init/release.
PREFERRED: disable animations globally via testOptions.animationsDisabled = true; do not toggle Settings.Global flags by hand.
PREFERRED: read every NoMatchingViewException message — it includes a hierarchy dump. The view you wanted may be present but in a different Root.
Verification
grep -r "Thread.sleep" src/androidTest/ returns nothing (or only screenshot-test files where the RenderThread is the wait target).
androidx/test/espresso/intent/Intents.java — init, release, intended, intending. androidx.test.espresso.intent.rule.IntentsRule is the modern JUnit4 wrapper.