Comprehensive mobile testing framework expertise
Generates and configures mobile test automation for Detox, Maestro, XCUITest, Espresso, and Appium frameworks.
npx claudepluginhub a5c-ai/babysitterThis skill inherits all available tools. When active, it can use any tool Claude has access to.
README.mdThis skill provides comprehensive expertise in mobile testing frameworks across platforms. It enables E2E testing with Detox and Maestro, native testing with XCUITest and Espresso, and cross-platform testing with Appium.
bash - Execute test commands and framework CLIsread - Analyze test files and configurationswrite - Generate test cases and configurationsedit - Update existing testsglob - Search for test filesgrep - Search for patterns in test codeConfiguration
Test Writing
Advanced Features
Flow Configuration
Actions and Assertions
Test Setup
UI Testing
Test Configuration
UI Testing
This skill integrates with the following processes:
mobile-testing-strategy.js - Testing strategy implementationmobile-accessibility-implementation.js - Accessibility testingmobile-security-implementation.js - Security testing// .detoxrc.js
module.exports = {
testRunner: {
args: {
$0: 'jest',
config: 'e2e/jest.config.js',
},
jest: {
setupTimeout: 120000,
},
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app',
build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
},
'ios.release': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/MyApp.app',
build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
reversePorts: [8081],
},
'android.release': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: { type: 'iPhone 15 Pro' },
},
emulator: {
type: 'android.emulator',
device: { avdName: 'Pixel_7_API_34' },
},
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug',
},
'ios.sim.release': {
device: 'simulator',
app: 'ios.release',
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug',
},
'android.emu.release': {
device: 'emulator',
app: 'android.release',
},
},
};
// e2e/login.test.ts
import { device, element, by, expect } from 'detox';
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp({
newInstance: true,
permissions: { notifications: 'YES' },
});
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should show login screen on first launch', async () => {
await expect(element(by.id('login-screen'))).toBeVisible();
await expect(element(by.id('email-input'))).toBeVisible();
await expect(element(by.id('password-input'))).toBeVisible();
});
it('should show error for invalid credentials', async () => {
await element(by.id('email-input')).typeText('invalid@example.com');
await element(by.id('password-input')).typeText('wrongpassword');
await element(by.id('login-button')).tap();
await expect(element(by.id('error-message'))).toBeVisible();
await expect(element(by.text('Invalid credentials'))).toBeVisible();
});
it('should navigate to home on successful login', async () => {
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await waitFor(element(by.id('home-screen')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.id('welcome-message'))).toBeVisible();
});
it('should handle scroll in long list', async () => {
// Login first
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await waitFor(element(by.id('home-screen'))).toBeVisible().withTimeout(5000);
// Scroll to bottom of list
await element(by.id('item-list')).scrollTo('bottom');
await expect(element(by.id('item-50'))).toBeVisible();
// Scroll back to top
await element(by.id('item-list')).scrollTo('top');
await expect(element(by.id('item-1'))).toBeVisible();
});
});
# flows/login.yaml
appId: com.example.myapp
---
- launchApp:
clearState: true
- assertVisible: "Welcome"
- tapOn: "Email"
- inputText: "test@example.com"
- tapOn: "Password"
- inputText: "password123"
- tapOn: "Sign In"
- assertVisible: "Home"
- takeScreenshot: "home_after_login"
# Test logout flow
- tapOn: "Profile"
- tapOn: "Sign Out"
- assertVisible: "Welcome"
// MyAppUITests/LoginTests.swift
import XCTest
final class LoginTests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["--uitesting"]
app.launch()
}
override func tearDownWithError() throws {
app = nil
}
func testLoginScreenElements() throws {
XCTAssertTrue(app.textFields["email-input"].exists)
XCTAssertTrue(app.secureTextFields["password-input"].exists)
XCTAssertTrue(app.buttons["login-button"].exists)
}
func testSuccessfulLogin() throws {
let emailField = app.textFields["email-input"]
let passwordField = app.secureTextFields["password-input"]
let loginButton = app.buttons["login-button"]
emailField.tap()
emailField.typeText("test@example.com")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
// Wait for home screen
let homeScreen = app.otherElements["home-screen"]
XCTAssertTrue(homeScreen.waitForExistence(timeout: 5))
}
func testInvalidCredentials() throws {
app.textFields["email-input"].tap()
app.textFields["email-input"].typeText("invalid@example.com")
app.secureTextFields["password-input"].tap()
app.secureTextFields["password-input"].typeText("wrong")
app.buttons["login-button"].tap()
XCTAssertTrue(app.staticTexts["Invalid credentials"].waitForExistence(timeout: 3))
}
func testScrollBehavior() throws {
// Navigate to list screen
app.buttons["list-tab"].tap()
let list = app.tables["item-list"]
let lastItem = app.cells["item-cell-50"]
// Scroll down
while !lastItem.isHittable {
list.swipeUp()
}
XCTAssertTrue(lastItem.exists)
// Scroll back up
let firstItem = app.cells["item-cell-1"]
while !firstItem.isHittable {
list.swipeDown()
}
XCTAssertTrue(firstItem.exists)
}
}
// app/src/androidTest/java/com/example/myapp/LoginTest.kt
package com.example.myapp
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class LoginTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun loginScreen_displaysAllElements() {
composeRule.onNodeWithTag("email-input").assertIsDisplayed()
composeRule.onNodeWithTag("password-input").assertIsDisplayed()
composeRule.onNodeWithTag("login-button").assertIsDisplayed()
}
@Test
fun login_withValidCredentials_navigatesToHome() {
composeRule.onNodeWithTag("email-input")
.performTextInput("test@example.com")
composeRule.onNodeWithTag("password-input")
.performTextInput("password123")
composeRule.onNodeWithTag("login-button")
.performClick()
composeRule.waitUntil(5000) {
composeRule.onAllNodesWithTag("home-screen")
.fetchSemanticsNodes().isNotEmpty()
}
composeRule.onNodeWithTag("home-screen").assertIsDisplayed()
}
@Test
fun login_withInvalidCredentials_showsError() {
composeRule.onNodeWithTag("email-input")
.performTextInput("invalid@example.com")
composeRule.onNodeWithTag("password-input")
.performTextInput("wrong")
composeRule.onNodeWithTag("login-button")
.performClick()
composeRule.onNodeWithText("Invalid credentials")
.assertIsDisplayed()
}
@Test
fun list_scrollsBehavior() {
// Login first
composeRule.onNodeWithTag("email-input")
.performTextInput("test@example.com")
composeRule.onNodeWithTag("password-input")
.performTextInput("password123")
composeRule.onNodeWithTag("login-button")
.performClick()
composeRule.waitUntil(5000) {
composeRule.onAllNodesWithTag("item-list")
.fetchSemanticsNodes().isNotEmpty()
}
// Scroll to item 50
composeRule.onNodeWithTag("item-list")
.performScrollToNode(hasTestTag("item-50"))
composeRule.onNodeWithTag("item-50").assertIsDisplayed()
}
}
# .github/workflows/android-test.yml
name: Android Tests
on: [push, pull_request]
jobs:
instrumented-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build APKs
run: |
./gradlew assembleDebug assembleDebugAndroidTest
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS }}
- name: Run tests on Firebase Test Lab
run: |
gcloud firebase test android run \
--type instrumentation \
--app app/build/outputs/apk/debug/app-debug.apk \
--test app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
--device model=Pixel6,version=33 \
--device model=Pixel7,version=34 \
--timeout 15m \
--results-bucket gs://my-test-results
accessibility-testing - Accessibility-focused testingmobile-perf - Performance testingfastlane-cicd - CI/CD integrationActivates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.