Phone Harness
ShawnPana/phone-harness
Control the user's phone — an iPhone through the Mac's iPhone Mirroring window, an Android over adb, or a rented cloud Android: open apps, tap, type, swipe, read the screen.
A skill your agent uses to write Espresso 3.7.0 tests against Android Views — onView(matcher).perform(action).check(matches(...)), onData(...) for AdapterView, RecyclerViewActions…
$ npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install skydoves/android-testing-skills writing-espresso-tests --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/instrumentation/espresso/writing-espresso-tests .claude/skills/writing-espresso-tests && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "writing-espresso-tests" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/espresso/writing-espresso-tests into .claude/skills/writing-espresso-tests/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "writing-espresso-tests", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/espresso/writing-espresso-testsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install skydoves/android-testing-skills writing-espresso-tests --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/instrumentation/espresso/writing-espresso-tests .agents/skills/writing-espresso-tests && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "writing-espresso-tests" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/espresso/writing-espresso-tests into .agents/skills/writing-espresso-tests/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "writing-espresso-tests", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install skydoves/android-testing-skills writing-espresso-tests --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/instrumentation/espresso/writing-espresso-tests .cursor/skills/writing-espresso-tests && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "writing-espresso-tests" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/espresso/writing-espresso-tests into .cursor/skills/writing-espresso-tests/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "writing-espresso-tests", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/skydoves/android-testing-skills.git --path instrumentation/espresso/writing-espresso-tests--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install skydoves/android-testing-skills writing-espresso-tests --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/instrumentation/espresso/writing-espresso-tests .gemini/skills/writing-espresso-tests && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "writing-espresso-tests" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/espresso/writing-espresso-tests into .gemini/skills/writing-espresso-tests/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "writing-espresso-tests", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install skydoves/android-testing-skills writing-espresso-testsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .github/skills && cp -r skills-src/instrumentation/espresso/writing-espresso-tests .github/skills/writing-espresso-tests && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "writing-espresso-tests" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/espresso/writing-espresso-tests into .github/skills/writing-espresso-tests/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "writing-espresso-tests", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install skydoves/android-testing-skills writing-espresso-tests --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/skydoves/android-testing-skills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/instrumentation/espresso/writing-espresso-tests .opencode/skills/writing-espresso-tests && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "writing-espresso-tests" agent skill from https://github.com/skydoves/android-testing-skills/tree/main/instrumentation/espresso/writing-espresso-tests into .opencode/skills/writing-espresso-tests/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "writing-espresso-tests", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
writing-espresso-testsA skill your agent uses to write Espresso 3.7.0 tests against Android Views — onView(matcher).perform(action).check(matches(...)), onData(...) for AdapterView, RecyclerViewActions…
Writing Espresso Tests is an agent skill from skydoves/android-testing-skills. 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)…
Its SKILL.md is about 5.4k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Mobile, covering Mobile testing and debugging. It works with Android. The repository describes itself as: ⚡️ A set of skills for Android testing: Compose UI, AndroidX Test, JVM unit tests, and ADB. The licence is Apache-2.0.
6 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 8665ed5. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are kotlin).
From the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
developer.android.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Writing Espresso Tests loads about 5.4k tokens when it runs. Until then it costs about 205 tokens; SKILL.md has 1,224 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from skydoves/android-testing-skills at commit 8665ed5, republished under its Apache-2.0 licence (© skydoves). 1,224 words, ~5,381 tokens.
.claude/skills/writing-espresso-tests/SKILL.md (or your agent's skills folder).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.
NoMatchingViewException, AmbiguousViewMatcherException, or PerformException.AlertDialog button silently does nothing — RootMatchers.DEFAULT excludes dialogs.Intent.startActivityForResult for camera, contacts, or share flows.IdlingResource for a long-running task that Espresso can't see.RecyclerView interaction.BottomNavigationViewActions and is surprised it isn't on the classpath in Espresso 3.7.0 contrib.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.InjectEventSecurityException from Espresso. Use ../../uiautomator/cross-app-tests-with-uiautomator/SKILL.md.../../runner/running-instrumented-tests-with-androidjunit4/SKILL.md.../../scenarios/launching-activities-with-activityscenario/SKILL.md.../../runner/running-instrumented-tests-with-androidjunit4/SKILL.md.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 // hermeticActivity (or Fragment) launched via ActivityScenario / ActivityScenarioRule / FragmentScenario.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):import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.*
import org.hamcrest.Matchers.allOf
onView(withId(R.id.submit_button))
onView(allOf(withText("Submit"), isEnabled()))
onView(withContentDescription("Open menu"))
onView(allOf(withId(R.id.subtitle), hasDescendant(withText("Hello")))) 2. Compose state-aware matchers. Common state matchers from ViewMatchers: isDisplayed, isCompletelyDisplayed, isDisplayingAtLeast(percent), isEnabled/isNotEnabled, isFocused, isChecked/isNotChecked, isSelected, withEffectiveVisibility(Visibility.VISIBLE|INVISIBLE|GONE), withAlpha(float). Hierarchy matchers: withParent, hasDescendant, withChild, isRoot, withClassName, isAssignableFrom(Class). Layout-relative: hasSibling, isDescendantOfA. Source: R3 lines 180-280.
3. Act with ViewActions via .perform(action). From androidx.test.espresso.action.ViewActions:
import androidx.test.espresso.action.ViewActions.*
onView(withId(R.id.email)).perform(typeText("test@example.com"), closeSoftKeyboard())
onView(withId(R.id.submit_button)).perform(click())
onView(withId(R.id.input)).perform(replaceText("new"), pressImeActionButton())
onView(withId(R.id.list)).perform(swipeUp())
onView(withId(R.id.scroller)).perform(scrollTo()) // requires single-direction ScrollViewClick family: click, longClick, doubleClick. Text: typeText, replaceText, clearText, pressKey, pressImeActionButton, closeSoftKeyboard. Gesture: swipeUp, swipeDown, swipeLeft, swipeRight, scrollTo. Source: androidx.test.espresso.action.ViewActions.
ViewAssertions via .check(...). From androidx.test.espresso.assertion.ViewAssertions:import androidx.test.espresso.assertion.ViewAssertions.*
import androidx.test.espresso.matcher.ViewMatchers.*
onView(withId(R.id.title)).check(matches(withText("Welcome")))
onView(withId(R.id.banner)).check(matches(isDisplayed()))
onView(withId(R.id.banner)).check(doesNotExist())
onView(withId(R.id.list)).check(selectedDescendantsMatch(withId(R.id.row), isCompletelyDisplayed()))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.
onView(...).perform(...) and .check(...) call internally:mainThreadExecutor.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.viewMatcher against the current Root's hierarchy). 0 → NoMatchingViewException; 2+ → AmbiguousViewMatcherException.viewAction.getConstraints().matches(view) → throws PerformException if false.viewAction.perform(uiController, view) (or viewAssertion.check(view, null)).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.
inRoot(...). RootMatchers.DEFAULT excludes dialogs, popups, and toasts (it matches "the focused activity window"). For an AlertDialog button:import androidx.test.espresso.matcher.RootMatchers.*
onView(withText("Submit")).inRoot(isDialog()).perform(click())
onView(withText("Copy")).inRoot(isPlatformPopup()).perform(click()) // overflow menu, popup window
onView(withText("Saved")).inRoot(isPlatformPopup()).check(matches(isDisplayed())) // toastRootMatchers.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.
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):import androidx.test.espresso.contrib.RecyclerViewActions.*
onView(withId(R.id.list))
.perform(scrollToPosition<RecyclerView.ViewHolder>(20))
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(3, click()))
onView(withId(R.id.list))
.perform(scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("Pikachu"))))
onView(withId(R.id.list))
.perform(actionOnItem<RecyclerView.ViewHolder>(hasDescendant(withText("Pikachu")), click()))All RecyclerViewActions require isAssignableFrom(RecyclerView.class) AND isDisplayed() constraint (R3 line 428).
AdapterView (ListView, Spinner), use onData(...) not onView(...).import androidx.test.espresso.Espresso.onData
import org.hamcrest.Matchers.allOf
onData(allOf(`is`(instanceOf(String::class.java)), `is`("Item 5")))
.inAdapterView(withId(R.id.list))
.perform(click())onData scrolls the adapter to bring the item on-screen before resolving its view.
Intents. Init in @Before, release in @After. The IntentsRule (3.5+) handles both:import androidx.test.espresso.intent.rule.IntentsRule
import androidx.test.espresso.intent.Intents.*
import androidx.test.espresso.intent.matcher.IntentMatchers.*
@get:Rule val intentsRule = IntentsRule()
@Test
fun openCameraStub() {
intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
onView(withId(R.id.take_photo)).perform(click())
intended(allOf(
hasAction(MediaStore.ACTION_IMAGE_CAPTURE),
hasFlag(Intent.FLAG_GRANT_READ_URI_PERMISSION),
))
}IntentsTestRule is @Deprecated per docs/CORPUS.md H.7 — use IntentsRule or call Intents.init() / Intents.release() manually.
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:class CountingIdlingResource(private val name: String) : IdlingResource {
private val counter = AtomicInteger(0)
private var callback: IdlingResource.ResourceCallback? = null
override fun getName() = name
override fun isIdleNow() = counter.get() == 0
override fun registerIdleTransitionCallback(cb: IdlingResource.ResourceCallback) { callback = cb }
fun increment() { counter.incrementAndGet() }
fun decrement() { if (counter.decrementAndGet() == 0) callback?.onTransitionToIdle() }
}
@Before fun setUp() = IdlingRegistry.getInstance().register(myIdler)
@After fun tearDown() = 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.setMasterPolicyTimeoutWhenDebuggerAttached(false)
// — there is no "-1 = infinite" magic value.
IdlingPolicies.setMasterPolicyTimeout(60, TimeUnit.SECONDS)
IdlingPolicies.setIdlingResourceTimeout(45, 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.// 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.// RIGHT
import androidx.test.espresso.matcher.RootMatchers.isDialog
onView(withText("Submit")).inRoot(isDialog()).perform(click())Same applies to popup menus (isPlatformPopup()) and toasts.
BottomNavigationView interaction (Espresso 3.7.0 contrib)// WRONG
import 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 directly
import 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())PickerActions.setDate month convention// WRONG
import 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))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)RecyclerView to an off-screen row before clicking itimport androidx.test.espresso.contrib.RecyclerViewActions.*
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
onView(withId(R.id.list))
.perform(scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("Snorlax"))))
onView(withId(R.id.list))
.perform(actionOnItem<RecyclerView.ViewHolder>(hasDescendant(withText("Snorlax")), click()))scrollTo and actionOnItem use a HOLDER matcher (matches the underlying RecyclerView.ViewHolder's itemView), not a Matcher<View> against the screen.
.inRoot(isDialog()) for any matcher targeting an AlertDialog content view, .inRoot(isPlatformPopup()) for popup menus and toasts. RootMatchers.DEFAULT excludes them.withId / withTagKey / withResourceName over withText. Text matchers are i18n-fragile and churn with copy edits.Thread.sleep to wait for animations or async work. Use IdlingResource + IdlingPolicies.setMasterPolicyTimeout, and testOptions.animationsDisabled = true.androidx.test.espresso.contrib.BottomNavigationViewActions — the class is not in espresso-contrib:3.7.0. Use the isDescendantOfA(isAssignableFrom(BottomNavigationView::class.java)) workaround.PickerActions.setDate(year, month, day). The action subtracts 1 internally before calling DatePicker.updateDate.Intents.init() / Intents.release() (or use IntentsRule) — intended / intending only work between those calls.:espresso-idling-resource:3.7.0 on implementation (production), not androidTestImplementation. The production code is what increments / decrements the resource.IntentsTestRule — it is @Deprecated. Use IntentsRule (3.5+) or manual init/release.testOptions.animationsDisabled = true; do not toggle Settings.Global flags by hand.NoMatchingViewException message — it includes a hierarchy dump. The view you wanted may be present but in a different Root.grep -r "Thread.sleep" src/androidTest/ returns nothing (or only screenshot-test files where the RenderThread is the wait target).grep -r "BottomNavigationViewActions" src/androidTest/ returns nothing.grep -r "IntentsTestRule" src/androidTest/ returns nothing — use IntentsRule or manual init/release.inRoot(isDialog()) matches an AlertDialog-class API in production; otherwise the test passes against the wrong root../gradlew :<module>:connectedDebugAndroidTest runs without NoMatchingViewException or PerformException from the catalog above.testOptions.animationsDisabled = true is set in the module's android block.IdlingResources call IdlingRegistry.getInstance().unregister(...) in @After (or use a JUnit rule).androidx/test/espresso/matcher/ViewMatchers.java (lines 79-1000+) — full matcher catalog. Identity (withId, withTagKey, withResourceName); text (withText, withSubstring, withHint); state (isDisplayed, isEnabled, isChecked, withEffectiveVisibility); hierarchy (withParent, hasDescendant, withChild, isRoot, isDescendantOfA).androidx/test/espresso/matcher/RootMatchers.java line 51 — DEFAULT = allOf(hasWindowLayoutParams(), allOf(anyOf(allOf(isDialog(), withDecorView(hasWindowFocus())), isSubwindowOfCurrentActivity()), isFocusable())). The trap.androidx/test/espresso/action/ViewActions.java — click, longClick, doubleClick, typeText, replaceText, clearText, pressKey, swipeUp/Down/Left/Right, scrollTo, pressImeActionButton, closeSoftKeyboard.androidx/test/espresso/assertion/ViewAssertions.java lines 51-118 — matches, doesNotExist, selectedDescendantsMatch.androidx/test/espresso/contrib/RecyclerViewActions.java lines 200-249 — scrollToPosition, scrollTo, actionOnItemAtPosition, actionOnItem. Constraint: isAssignableFrom(RecyclerView) AND isDisplayed.androidx/test/espresso/contrib/PickerActions.java lines 36-100 — setDate(year, monthOfYear, dayOfMonth) with monthOfYear 1-12 (line 49 KDoc). setTime(hours, minutes).androidx/test/espresso/IdlingPolicies.java lines 96-112 — setMasterPolicyTimeout, setIdlingResourceTimeout, setMasterPolicyTimeoutWhenDebuggerAttached.androidx/test/espresso/intent/Intents.java — init, release, intended, intending. androidx.test.espresso.intent.rule.IntentsRule is the modern JUnit4 wrapper.tasks/research/R3-espresso.md — full Espresso 3.7.0 deep-dive. Lines 91-93 (BottomNavigationViewActions absent), 311 (RootMatchers.DEFAULT), 456-460 (PickerActions month convention), 811-820 (idle-then-act).docs/CORPUS.md Section H.5 — Espresso essentials and RootMatchers.DEFAULT exclusions.© skydoves, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in instrumentation/espresso/writing-espresso-tests of skydoves/android-testing-skills.
Open the folder on GitHubat commit 8665ed5
Writing Espresso Tests next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Writing Espresso Tests this skillskydoves/android-testing-skills | 333 | — | ~5.4k | Automated safety check: Pass | Apache-2.0 | |
| Phone HarnessShawnPana/phone-harness | 3.2k | — | ~4.3k | Automated safety check: Pass | MIT | |
| Mobile QAtloncorp/tlon-apps | 107 | — | ~2.4k | Automated safety check: Pass | MIT | |
| Androidyang1ming/android-harness | 176 | — | ~259 | Automated safety check: Pass | MIT | |
| Debug Receiverstimusus/Shuttle2 | 229 | — | ~1.9k | Automated safety check: Pass | Apache-2.0 | |
| Debug Bridgegetknit/knit | 132 | — | ~2.2k | Automated safety check: Pass | GPL-3.0 |
ShawnPana/phone-harness
Control the user's phone — an iPhone through the Mac's iPhone Mirroring window, an Android over adb, or a rented cloud Android: open apps, tap, type, swipe, read the screen.
tloncorp/tlon-apps
Run a mobile QA checklist on a physical Android device over adb for tlon-apps, then triage what fails into fixes.
yang1ming/android-harness
Direct Android device control through ADB. An agent skill from yang1ming/android-harness.
timusus/Shuttle2
Drive the S2 debug build's playback and queue over ADB broadcasts — play the whole library, play/pause, skip, seek, remove a queue item, toggle shuffle/repeat, dump playback state as JSON, reimport…
getknit/knit
Drive and verify Knit on a device or emulator through the headless debug bridge (am broadcast to app.getknit.knit.debug.<ACTION, replies as JSON) — send a message on one phone and confirm it landed…
Moustachauve/WLED-Android
Production-ready scripts for Android app testing, building, and automation.
skydoves/android-testing-skills
A skill your agent uses to verify Compose layout measurements from a UI test using assertWidthIsEqualTo, assertHeightIsEqualTo, assertWidthIsAtLeast, assertHeightIsAtLeast…
skydoves/android-testing-skills
A skill your agent uses to verify a Compose semantics node's properties from a UI test using assertExists, assertDoesNotExist, assertIsDisplayed, assertIsNotDisplayed, assertIsDeactivated…
skydoves/android-testing-skills
A skill your agent uses to render every Jetpack Compose @Preview as a screenshot on a real Android device or emulator and publish a browsable HTML catalog from CI.
skydoves/android-testing-skills
A skill your agent uses to capture visual artefacts from a device for test failures, golden image generation, QA repro, and demo videos.
skydoves/android-testing-skills
A skill your agent uses to pick the correct Compose UI test entry point.
skydoves/android-testing-skills
A skill your agent uses to pick which behaviors to cover in an Android test suite using Google's five-category state vocabulary plus the explicit "what NOT to test" list from…
Works with
Categories
A skill your agent uses to write Espresso 3.7.0 tests against Android Views — onView(matcher).perform(action).check(matches(...)), onData(...) for AdapterView, RecyclerViewActions…. Writing Espresso Tests is an agent skill from skydoves/android-testing-skills.intended/intending, and IdlingResource registration.
Writing Espresso Tests fits situations like: write Espresso 3.7.0 tests against Android Views — onView(matcher).perform(action).check(matches(...)); onData(...) for AdapterView; recyclerViewActions; intents.intended/intending.
Run `npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a claude-code`. Or copy the skill folder (instrumentation/espresso/writing-espresso-tests in skydoves/android-testing-skills) into .claude/skills/writing-espresso-tests in your project. Claude Code loads it when a task matches its description.
Run `npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a codex`. Or copy the skill folder (instrumentation/espresso/writing-espresso-tests in skydoves/android-testing-skills) into .agents/skills/writing-espresso-tests in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add skydoves/android-testing-skills --skill writing-espresso-tests -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/writing-espresso-tests, .gemini/skills/writing-espresso-tests, .github/skills/writing-espresso-tests and .opencode/skills/writing-espresso-tests in your project.
SKILL.md names no scripts, command-line tools or credentials: Writing Espresso Tests is instructions for the agent only.
SKILL.md names 1 domain. As links in the text: developer.android.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Writing Espresso Tests is published under the Apache-2.0 licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 5.4k tokens (SKILL.md is roughly 22k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Writing Espresso Tests: Phone Harness (ShawnPana/phone-harness, 3.2k stars), Mobile QA (tloncorp/tlon-apps, 107 stars), Android (yang1ming/android-harness, 176 stars) and Debug Receivers (timusus/Shuttle2, 229 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
skydoves (a GitHub user) maintains it in skydoves/android-testing-skills, which has 333 GitHub stars. The repository holds 50 skills in this directory. The repository was last updated on May 25, 2026.
Source: skydoves/android-testing-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.