agents/skills/android-testing-tools/SKILL.md
Android UI testing toolkit with screenshot validation. Use when: (1) Setting up UI test infrastructure with Page Object pattern (2) Creating test tags/accessibility IDs with structured naming (3) Writing UI tests with step-by-step screenshots (4) Validating UI via screenshot comparison (5) Writing snapshot tests with Paparazzi or Shot (6) Comparing snapshot diffs with snapshotsdiff CLI (7) Integrating with Allure for test reporting (8) Organizing shared test identifiers between app and test targets File types: Kotlin UI tests, Espresso, UIAutomator, Compose UI Test, Allure reports
npx skillsauth add relux-works/skill-android-testing-tools android-testing-toolsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Toolkit for Android UI testing with screenshot validation and Page Object pattern.
// Pattern: {Module}_{Screen}_{Element}_{Type}
// Examples:
"Auth_Login_Username_input"
"Auth_Login_Submit_button"
"Home_Feed_Post_card"
"Settings_Profile_Avatar_image"
// In Compose:
Modifier.testTag("Auth_Login_Username_input")
// In XML:
android:contentDescription="@string/auth_login_username_input"
class LoginTest : BaseUiTestSuite() {
override val packageName = "com.example.app"
@Test
fun testSuccessfulLogin() {
launchApp()
screenshot(1, "app_launched")
val loginPage = LoginPage(device).waitForReady()
screenshot(2, "login_page_ready")
val homePage = loginPage.login("[email protected]", "password123")
screenshot(3, "logged_in")
assertPageDisplayed(homePage)
}
}
class LoginPage(override val device: UiDevice) : PageElement {
override val readyMarker = "Auth_Login_Title_text"
val usernameField: UiObject2?
get() = device.findObject(By.res("Auth_Login_Username_input"))
val passwordField: UiObject2?
get() = device.findObject(By.res("Auth_Login_Password_input"))
val loginButton: UiObject2?
get() = device.findObject(By.res("Auth_Login_Submit_button"))
fun login(username: String, password: String): HomePage {
usernameField?.text = username
passwordField?.text = password
loginButton?.click()
return HomePage(device).waitForReady()
}
}
Add to your build.gradle.kts:
// In your app's androidTest dependencies
androidTestImplementation("com.uitesttools:screenshot-kit:1.0.0")
androidTestImplementation("com.uitesttools:uitest-kit:1.0.0")
// Standard test dependencies
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
Screenshots are saved in session folders with structured names:
/sdcard/Pictures/Screenshots/UITests/
└── Run_{session}/
├── Run_{session}__Test_{name}__Step_{NN}__{timestamp}__{description}.png
└── ...
Example:
/sdcard/Pictures/Screenshots/UITests/
└── Run_20240115_143022/
├── Run_20240115_143022__Test_testLogin__Step_01__143025_123__initial_screen.png
├── Run_20240115_143022__Test_testLogin__Step_02__143026_456__filled_form.png
└── Run_20240115_143022__Test_testLogout__Step_01__143030_789__logged_out.png
Each test run creates a new Run_{session}/ folder, keeping runs separated.
After extraction with the CLI, can be further organized into:
screenshots/
├── Run_20240115_143022/
│ ├── Test_testLogin/
│ │ ├── Step_01_initial_screen.png
│ │ └── Step_02_filled_form.png
│ └── Test_testLogout/
│ └── Step_01_logged_out.png
# From device to local directory
./Scripts/extract-screenshots.sh ./screenshots
# With options
./Scripts/extract-screenshots.sh ./screenshots \
--serial emulator-5554 \
--clean
# Run tests and extract in one command
./Scripts/run-tests-and-extract.sh \
-module app \
-testClass com.example.LoginTest \
-output ./screenshots
# Compare two images
snapshotsdiff reference.png actual.png diff.png
# Batch compare failed snapshots
snapshotsdiff \
--artifacts ./SnapshotArtifacts \
--output ./SnapshotDiffs \
--tests ./AppSnapshotTests
# SDK typically at:
~/Library/Android/sdk/
# Key binaries:
~/Library/Android/sdk/emulator/emulator
~/Library/Android/sdk/platform-tools/adb
~/Library/Android/sdk/cmdline-tools/latest/bin/avdmanager
# List available AVDs (Android Virtual Devices)
emulator -list-avds
# Start emulator (with GUI)
emulator -avd Pixel_9_Pro_XL
# Start emulator headless (no window, for CI)
emulator -avd Pixel_9_Pro_XL -no-audio -no-window &
# Wait for emulator to boot
adb wait-for-device
adb shell getprop sys.boot_completed # returns 1 when ready
# List connected devices/emulators
adb devices
# Check device is ready
adb shell getprop ro.product.model
adb shell getprop ro.build.version.release
# Install APK
adb install app-debug.apk
adb install -r app-debug.apk # reinstall
# Uninstall
adb uninstall com.example.app
# Screenshots saved to device at:
/sdcard/Pictures/Screenshots/UITests/
# List screenshots on device
adb shell "ls -la /sdcard/Pictures/Screenshots/UITests/"
# Pull all screenshots
adb pull /sdcard/Pictures/Screenshots/UITests/ ./screenshots/
# Clean screenshots from device
adb shell "rm -rf /sdcard/Pictures/Screenshots/UITests/*"
# Run all tests in module
./gradlew :app:connectedDebugAndroidTest
# Run specific test class
./gradlew :app:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.example.LoginTest
# Run specific test method
./gradlew :app:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.example.LoginTest#testSuccessfulLogin
UIAutomator cannot see Compose testTag by default!
You MUST add testTagsAsResourceId = true to root composable:
// MainActivity.kt
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalComposeUiApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier
.fillMaxSize()
.semantics { testTagsAsResourceId = true } // <-- REQUIRED!
) {
// Your app content
}
}
}
}
}
Without this, device.findObject(By.res("MyTag")) will return null!
// In your Composable
@Composable
fun LoginScreen() {
Column {
TextField(
modifier = Modifier.testTag("Auth_Login_Username_input"),
// ...
)
Button(
modifier = Modifier.testTag("Auth_Login_Submit_button"),
onClick = { /* ... */ }
) {
Text("Login")
}
}
}
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testLoginButton() {
composeTestRule.setContent {
LoginScreen()
}
composeTestRule
.onNodeWithTag("Auth_Login_Submit_button")
.assertIsDisplayed()
.performClick()
}
// UIAutomator
device.waitForResourceId("Auth_Login_Title_text", timeout = 5000)
// Compose
composeTestRule.waitForTag("Auth_Login_Title_text", timeout = 5000)
// Espresso with extension
onView(withId(R.id.loginButton))
.waitUntilEnabled(timeout = 5000)
// UIAutomator - click right side of switch
element.click(Point(element.visibleBounds.right - 10, element.visibleCenter.y))
// Compose
onNodeWithTag("Settings_Notifications_Toggle_switch")
.clickAtOffset(0.9f, 0.5f)
// Espresso
onView(withId(R.id.switch))
.clickAtOffset(0.9f, 0.5f)
// UIAutomator
device.scrollDownUntilFound(By.res("Settings_Advanced_button"))?.click()
// Compose
onNodeWithTag("Settings_Advanced_button")
.scrollToAndClick()
// Espresso
onView(withId(R.id.advancedButton))
.scrollToAndClick()
build.gradle.ktstestTag constants in shared module (see @assets/TestEnvShared/)BaseUiTestSuite{Module}_{Screen}_{Element}_{Type}_button, _input, _text, _image, _cardreadyMarker to wait for page load@BeforeClassassertPageDisplayed() for navigation verificationwaitFor() instead of Thread.sleep()tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
A CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.