espresso-suite
Authors Espresso UI tests for Android - uses `onView(withId(...)).perform(...).check(matches(...))`, leans on Espresso's automatic synchronization (no `Thread.sleep`), wires `IdlingResource` for app-specific async, runs via `./gradlew connectedAndroidTest` and parses the JUnit XML output. Use when an Android app needs UI tests in Google's first-party framework.
Install with skills.sh (any agent)
npx skills add testland/qa --skill espresso-suiteespresso-suite
Overview
Per espresso-doc (opens in new window):
"Espresso is a framework for writing concise, beautiful, and reliable Android UI tests."
The canonical example:
@Test
fun greeterSaysHello() {
onView(withId(R.id.name_field)).perform(typeText("Steve"))
onView(withId(R.id.greet_button)).perform(click())
onView(withText("Hello Steve!")).check(matches(isDisplayed()))
}The shape: onView(<matcher>).perform(<action>).check(<matches>).
The framework auto-waits via "synchronization conditions" per espresso-doc (opens in new window):
"Each time a test invokes onView(), Espresso waits to perform the corresponding UI action or assertion until these synchronization conditions are met:
This eliminates the need for Thread.sleep for stock Android async work.
When to use
Step 1 - Add dependencies
// app/build.gradle
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
androidTestImplementation 'androidx.test:rules:1.6.1'
}Per espresso-doc (opens in new window), Espresso ships as multiple packages:
| Package | Purpose |
|---|---|
espresso-core | Core View matchers, actions, assertions. |
espresso-web | WebView support. |
espresso-idling-resource | Synchronization for background jobs. |
espresso-contrib | DatePicker, RecyclerView, Drawer actions, accessibility checks. |
espresso-intents | Intent validation + stubbing. |
espresso-remote | Multi-process functionality. |
Step 2 - Author tests
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 androidx.test.ext.junit.rules.ActivityScenarioRule
@RunWith(AndroidJUnit4::class)
class CheckoutTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun applyPromoCode() {
onView(withId(R.id.promo_field)).perform(typeText("WELCOME10"), closeSoftKeyboard())
onView(withId(R.id.apply_button)).perform(click())
onView(withId(R.id.subtotal)).check(matches(withText("$22.49")))
}
}Step 3 - IdlingResource for app-specific async
When the app uses something Espresso doesn't auto-track (e.g. custom thread pool, RxJava chains), wire an IdlingResource:
class NetworkIdlingResource : IdlingResource {
@Volatile private var callback: IdlingResource.ResourceCallback? = null
@Volatile private var pendingRequests = 0
override fun getName() = "NetworkIdlingResource"
override fun isIdleNow() = pendingRequests == 0
override fun registerIdleTransitionCallback(cb: IdlingResource.ResourceCallback) {
callback = cb
}
fun increment() = synchronized(this) { pendingRequests++ }
fun decrement() = synchronized(this) {
pendingRequests--
if (pendingRequests == 0) callback?.onTransitionToIdle()
}
}
// Wire from production code at network call sites:
networkIdlingResource.increment()
api.fetchOrders().enqueue { _, _ -> networkIdlingResource.decrement() }
// Register in test setup:
@Before fun setup() { IdlingRegistry.getInstance().register(networkIdlingResource) }
@After fun teardown() { IdlingRegistry.getInstance().unregister(networkIdlingResource) }Without IdlingResource, Espresso assumes idle when its built-in trackers report idle - async work the framework can't see triggers flake.
Step 4 - Common matchers and actions
// Matchers
withId(R.id.button) // by ID
withText("Submit") // by displayed text
withContentDescription("Login") // by content description
hasSibling(withText("Email")) // structural relationship
isDisplayed() // currently visible
// Actions
click(), longClick()
typeText("..."), clearText()
scrollTo(), swipeLeft(), swipeUp()
pressBack()
// Assertions
matches(withText("Hello"))
matches(isDisplayed())
matches(isEnabled())
doesNotExist()Step 5 - RecyclerView interactions
// espresso-contrib provides RecyclerViewActions
onView(withId(R.id.cart_list))
.perform(RecyclerViewActions.actionOnItemAtPosition<MyViewHolder>(0, click()))
onView(withId(R.id.cart_list))
.perform(RecyclerViewActions.scrollTo<MyViewHolder>(hasDescendant(withText("BOOK-001"))))Step 6 - Run
# Unit + UI tests on connected device / emulator
./gradlew connectedAndroidTest
# With test orchestrator (each test in fresh process - preferred for isolation):
android {
defaultConfig {
testInstrumentationRunnerArguments clearPackageData: 'true'
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}Step 7 - CI integration (Android emulator on GitHub Actions)
jobs:
ui-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
script: ./gradlew connectedAndroidTest
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: app/build/outputs/androidTest-resultsThe test results land in JUnit XML at app/build/outputs/androidTest-results/.../TEST-*.xml - consumable by junit-xml-analysis (in the qa-test-reporting plugin).
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Thread.sleep for async waits | Espresso has built-in sync; sleeps mask race conditions. | Use IdlingResource for unsupported async (Step 3). |
withText("Submit") for buttons | Translation breaks the test. | withId(R.id.submit_button) or withContentDescription. |
Missing closeSoftKeyboard() after typing | Keyboard obscures next view; click() misses. | Chain closeSoftKeyboard() after typeText. |
Skipping IdlingResource for Retrofit / OkHttp | Network async invisible; flaky. | Wire IdlingResource (Step 3) OR use OkHttp's IdlingResource bridge. |
| One mega-test that asserts 10 things | One failure obscures others; debugging hard. | One assertion target per @Test. |
| Skipping test orchestrator | Tests share process; one test's state leaks. | Enable ANDROIDX_TEST_ORCHESTRATOR (Step 6). |
Limitations
References
Related skills
appium-testing
Wires Appium for cross-platform mobile UI automation - uses the WebDriver protocol, picks a driver per platform (XCUITest for iOS, UiAutomator2 / Espresso for Android, Mac2 for macOS, Windows for desktop), authors tests in JS / Python / Java / Ruby / .NET, configures `desiredCapabilities`, runs against simulators / emulators / device farms. Use when a single test suite must cover both iOS and Android, or when the team's stack is multi-platform (iOS + Android + Mac + Windows).
detox-testing
Authors React Native E2E tests with Detox (Wix) - gray-box architecture (runs in-process with the app), `element(by.id|by.text|by.label)` matchers, `waitFor()` for explicit sync beyond Detox's automatic async tracking, Jest runner. Use when the app is React Native and speed matters. For Flutter use flutter-testing; for black-box cross-platform use appium-testing; for YAML-declarative flows use maestro-flows; for non-RN native use xcuitest-suite or espresso-suite.
flutter-testing
Authors Flutter tests across the three-layer pyramid - unit (`flutter test`), widget (`testWidgets` + `WidgetTester`), integration (`integration_test` on simulator/emulator/device). Picks the right layer per change, mocks via `mockito` + `build_runner`, LCOV coverage, CI with the Flutter Action. Use when the app is Flutter and the team wants its first-party stack. For React Native use detox-testing; for black-box cross-platform use appium-testing; for YAML-declarative flows use maestro-flows.
maestro-flows
Authors Maestro YAML flow files (`.maestro/*.yaml`) for mobile + web UI automation: declarative `tapOn`, `inputText`, `assertVisible`, `swipe`, supported targets (iOS, Android, Flutter, React Native, web), nested flow imports, JavaScript hooks for complex conditions. Use when the team has already chosen Maestro, is coming from an existing `.maestro/` directory, or explicitly wants YAML-declarative tests readable by non-engineers without a compile step. For framework selection or authoring tests in XCUITest / Espresso / Detox / Appium / Flutter, use a mobile driver-selection or per-flow mobile test-authoring step instead.
mobile-a11y-test-author
Authors native mobile accessibility tests covering iOS (Accessibility Inspector, XCUITest `performAccessibilityAudit()` introduced in iOS 17, VoiceOver label/trait/hint verification) and Android (Espresso `AccessibilityChecks.enable()`, Accessibility Scanner, TalkBack traversal, `contentDescription` labelling) with WCAG-aligned checks for element labels, 44pt/48dp touch targets, contrast ratios, and focus order. Use when an iOS or Android app needs automated and manual accessibility test coverage beyond what `xcuitest-suite` or `espresso-suite` provide.
mobile-device-matrix-toolkit
Dispatches mobile UI test runs across a 3-tier device matrix (smoke per-PR, regression per-merge, full farm at release) to control CI cost: generates per-target Appium capability configs from a central YAML, parallelises via GitHub Actions matrix strategy, and aggregates JUnit XML into a cross-device pass/fail table. Use when deciding which iOS / Android devices and OS versions to run tests on and at which stage (smoke / regression / full farm), not how to configure a specific test framework (for that, use xcuitest-suite, espresso-suite, etc.).
mobile-web-emulation-runner
Builds a workflow to run web E2E tests under mobile viewports + DPRs (device pixel ratios): Playwright's `devices` catalog (iPhone 15, Pixel 7), suite run per-device as matrix shards, per-device screenshots, mobile assertions (`.tap()`, viewport-conditional layout). Use when a responsive web app needs mobile-breakpoint regression without a real-device farm. Mobile WEB only - for native apps use appium-testing, detox-testing, or flutter-testing; for cross-shard aggregation use mobile-device-matrix-toolkit; for gesture sequences use touch-gesture-tester.
mobile-web-perf-budget
Pure-reference skill for mobile-web performance budgets - Core Web Vitals at the 75th percentile mobile (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1; FID retired March 2024 in favor of INP), Lighthouse mobile profile config, per-route resource budgets (JS bundle, image weight, font load). Use as the team's reference for "what should the mobile perf gate enforce" - paired with `lighthouse-perf` (the runner) and `lighthouse-budget-author` (the per-route author).
touch-gesture-tester
Verifies touch-gesture handlers (tap, double-tap, long-press, swipe, pinch, rotate, pan) work as expected under both mobile-emulation (Playwright) and native (XCUITest / Espresso / Detox) - distinguishes "mouse click handler also fires on tap" from "real touch event fired with correct properties." Use when the app has bespoke gesture handlers (custom carousels, sliders, drag-drop, pull-to-refresh) and the team needs targeted gesture verification beyond generic UI assertions.
xcuitest-suite
Authors XCUIest UI tests for iOS / iPadOS / tvOS - uses the three-class XCUIApplication / XCUIElement / XCUIElementQuery pattern, sets accessibility identifiers on production code, runs via `xcodebuild test` with destination, parses the `xcresult` bundle. Use when an iOS app needs UI tests in Apple's first-party framework (no external runtime; native to Xcode).