$npx -y skills add LambdaTest/agent-skills --skill espresso-skillGenerates Espresso UI tests for Android apps in Kotlin or Java. Espresso runs inside the app process for fast, reliable UI testing. Supports local and TestMu AI cloud real devices. Use when user mentions "Espresso", "onView", "ViewMatchers", "Android UI test", or "instrumentation
| 1 | # Espresso Automation Skill |
| 2 | |
| 3 | You are a senior Android QA engineer specializing in Espresso UI testing. |
| 4 | |
| 5 | ## Step 1 — Execution Target |
| 6 | |
| 7 | ``` |
| 8 | ├─ Mentions "cloud", "TestMu", "LambdaTest", "device farm"? |
| 9 | │ └─ TestMu AI cloud (upload APK + test APK) |
| 10 | │ |
| 11 | ├─ Mentions "emulator", "local", "connected device"? |
| 12 | │ └─ Local: ./gradlew connectedAndroidTest |
| 13 | │ |
| 14 | └─ Default → Local emulator |
| 15 | ``` |
| 16 | |
| 17 | ## Core Patterns — Kotlin (Default) |
| 18 | |
| 19 | ### Basic Test |
| 20 | |
| 21 | ```kotlin |
| 22 | @RunWith(AndroidJUnit4::class) |
| 23 | class LoginTest { |
| 24 | |
| 25 | @get:Rule |
| 26 | val activityRule = ActivityScenarioRule(LoginActivity::class.java) |
| 27 | |
| 28 | @Test |
| 29 | fun loginWithValidCredentials() { |
| 30 | // Type email |
| 31 | onView(withId(R.id.emailInput)) |
| 32 | .perform(typeText("user@test.com"), closeSoftKeyboard()) |
| 33 | |
| 34 | // Type password |
| 35 | onView(withId(R.id.passwordInput)) |
| 36 | .perform(typeText("password123"), closeSoftKeyboard()) |
| 37 | |
| 38 | // Click login button |
| 39 | onView(withId(R.id.loginButton)) |
| 40 | .perform(click()) |
| 41 | |
| 42 | // Verify dashboard is displayed |
| 43 | onView(withId(R.id.dashboardTitle)) |
| 44 | .check(matches(isDisplayed())) |
| 45 | .check(matches(withText("Welcome"))) |
| 46 | } |
| 47 | |
| 48 | @Test |
| 49 | fun loginWithInvalidCredentials_showsError() { |
| 50 | onView(withId(R.id.emailInput)) |
| 51 | .perform(typeText("wrong@test.com"), closeSoftKeyboard()) |
| 52 | onView(withId(R.id.passwordInput)) |
| 53 | .perform(typeText("wrong"), closeSoftKeyboard()) |
| 54 | onView(withId(R.id.loginButton)) |
| 55 | .perform(click()) |
| 56 | onView(withId(R.id.errorText)) |
| 57 | .check(matches(isDisplayed())) |
| 58 | .check(matches(withText(containsString("Invalid")))) |
| 59 | } |
| 60 | } |
| 61 | ``` |
| 62 | |
| 63 | ### ViewMatchers (Finding Elements) |
| 64 | |
| 65 | ```kotlin |
| 66 | // By ID (best) |
| 67 | onView(withId(R.id.loginButton)) |
| 68 | |
| 69 | // By text |
| 70 | onView(withText("Login")) |
| 71 | |
| 72 | // By content description (accessibility) |
| 73 | onView(withContentDescription("Submit form")) |
| 74 | |
| 75 | // By hint text |
| 76 | onView(withHint("Enter your email")) |
| 77 | |
| 78 | // Combined matchers |
| 79 | onView(allOf(withId(R.id.button), withText("Submit"), isDisplayed())) |
| 80 | |
| 81 | // In RecyclerView |
| 82 | onView(withId(R.id.recyclerView)) |
| 83 | .perform(RecyclerViewActions.actionOnItemAtPosition<ViewHolder>(0, click())) |
| 84 | |
| 85 | // By parent |
| 86 | onView(allOf(withText("Delete"), isDescendantOfA(withId(R.id.toolbar)))) |
| 87 | ``` |
| 88 | |
| 89 | ### ViewActions (Performing Actions) |
| 90 | |
| 91 | ```kotlin |
| 92 | .perform(click()) // Tap |
| 93 | .perform(longClick()) // Long press |
| 94 | .perform(typeText("hello")) // Type text |
| 95 | .perform(replaceText("new text")) // Replace text |
| 96 | .perform(clearText()) // Clear field |
| 97 | .perform(closeSoftKeyboard()) // Dismiss keyboard |
| 98 | .perform(scrollTo()) // Scroll to element |
| 99 | .perform(swipeUp()) // Swipe gesture |
| 100 | .perform(swipeDown()) |
| 101 | .perform(swipeLeft()) |
| 102 | .perform(swipeRight()) |
| 103 | .perform(pressBack()) // Back button |
| 104 | ``` |
| 105 | |
| 106 | ### ViewAssertions (Checking State) |
| 107 | |
| 108 | ```kotlin |
| 109 | .check(matches(isDisplayed())) // Visible |
| 110 | .check(matches(not(isDisplayed()))) // Not visible |
| 111 | .check(matches(withText("Expected"))) // Text matches |
| 112 | .check(matches(isEnabled())) // Enabled |
| 113 | .check(matches(isChecked())) // Checkbox checked |
| 114 | .check(matches(hasErrorText("Required"))) // Error text |
| 115 | .check(doesNotExist()) // Not in hierarchy |
| 116 | ``` |
| 117 | |
| 118 | ### Idling Resources (Async Operations) |
| 119 | |
| 120 | ```kotlin |
| 121 | // Register before test |
| 122 | @Before |
| 123 | fun setUp() { |
| 124 | IdlingRegistry.getInstance().register(myIdlingResource) |
| 125 | } |
| 126 | |
| 127 | // Unregister after test |
| 128 | @After |
| 129 | fun tearDown() { |
| 130 | IdlingRegistry.getInstance().unregister(myIdlingResource) |
| 131 | } |
| 132 | |
| 133 | // Custom IdlingResource for network calls |
| 134 | class NetworkIdlingResource : IdlingResource { |
| 135 | private var callback: IdlingResource.ResourceCallback? = null |
| 136 | private var isIdle = true |
| 137 | |
| 138 | override fun getName() = "NetworkIdlingResource" |
| 139 | override fun isIdleNow() = isIdle |
| 140 | override fun registerIdleTransitionCallback(callback: ResourceCallback) { |
| 141 | this.callback = callback |
| 142 | } |
| 143 | |
| 144 | fun setIdle(idle: Boolean) { |
| 145 | isIdle = idle |
| 146 | if (idle) callback?.onTransitionToIdle() |
| 147 | } |
| 148 | } |
| 149 | ``` |
| 150 | |
| 151 | ### Anti-Patterns |
| 152 | |
| 153 | | Bad | Good | Why | |
| 154 | |-----|------|-----| |
| 155 | | `Thread.sleep()` | IdlingResources | Espresso auto-syncs UI thread | |
| 156 | | XPath-like traversal | `withId(R.id.x)` | Direct ID is fastest | |
| 157 | | Testing across activities | Test single screen, mock data | Isolatio |