Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Comprehensive mobile testing framework expertise
.claude/skills/a5c-ai-mobile-testing-frameworks/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 248% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 335% | 0% |
| case-19 | ✓→✗ | ▼ Worse | 166% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 99% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 134% | 0% |
This 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 codeThis skill integrates with the following processes:
mobile-testing-strategy.js - Testing strategy implementationmobile-accessibility-implementation.js - Accessibility testingmobile-security-implementation.js - Security testingjavascript// .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', }, }, };
typescript// 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(); }); });
yaml# 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"
swift// 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) } }
kotlin// 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() } }
yaml# .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 integration| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 16,725 | 46,709 | +179% | 1 | 1 | 0% | 3,277 | 6,527 | +99% | 0 | 0 | — |
case-02 | fail→pass | 7,257 | 4,520 | -38% | 1 | 1 | 0% | 1,348 | 4,685 | +248% | 0 | 0 | — |
case-03 | pass→pass | 13,319 | 5,522 | -59% | 1 | 1 | 0% | 2,115 | 4,953 | +134% | 0 | 0 | — |
case-04 | pass→pass | 3,519 | 3,287 | -7% | 1 | 1 | 0% | 603 | 4,388 | +628% | 0 | 0 | — |
case-05 | fail→pass | 5,747 | 3,442 | -40% | 1 | 1 | 0% | 997 | 4,340 | +335% | 0 | 0 | — |
case-06 | pass→pass | 2,690 | 2,690 | 0% | 1 | 1 | 0% | 489 | 4,242 | +767% | 0 | 0 | — |
case-07 | pass→pass | 5,883 | 3,362 | -43% | 1 | 1 | 0% | 842 | 4,322 | +413% | 0 | 0 | — |
case-08 | pass→pass | 4,460 | 2,197 | -51% | 1 | 1 | 0% | 639 | 4,218 | +560% | 0 | 0 | — |
case-09 | pass→pass | 4,888 | 4,076 | -17% | 1 | 1 | 0% | 1,069 | 4,543 | +325% | 0 | 0 | — |
case-10 | pass→pass | 6,795 | 4,016 | -41% | 1 | 1 | 0% | 1,315 | 4,579 | +248% | 0 | 0 | — |
case-11 | pass→pass | 8,990 | 7,088 | -21% | 1 | 1 | 0% | 1,685 | 5,233 | +211% | 0 | 0 | — |
case-12 | pass→pass | 13,010 | 11,489 | -12% | 1 | 1 | 0% | 2,446 | 6,056 | +148% | 0 | 0 | — |
case-13 | pass→pass | 5,994 | 4,920 | -18% | 1 | 1 | 0% | 1,249 | 4,791 | +284% | 0 | 0 | — |
case-14 | pass→pass | 4,314 | 4,925 | +14% | 1 | 1 | 0% | 794 | 4,783 | +502% | 0 | 0 | — |
case-15 | pass→pass | 4,264 | 3,899 | -9% | 1 | 1 | 0% | 812 | 4,534 | +458% | 0 | 0 | — |
case-16 | pass→pass | 8,325 | 6,109 | -27% | 1 | 1 | 0% | 1,593 | 5,030 | +216% | 0 | 0 | — |
case-17 | pass→pass | 18,030 | 14,050 | -22% | 1 | 1 | 0% | 2,907 | 6,106 | +110% | 0 | 0 | — |
case-18 | pass→pass | 16,941 | 4,385 | -74% | 1 | 1 | 0% | 2,585 | 4,460 | +73% | 0 | 0 | — |
case-19 | pass→fail | 7,606 | 4,996 | -34% | 1 | 1 | 0% | 1,491 | 3,965 | +166% | 0 | 0 | — |
case-20 | pass→pass | 10,846 | 13,281 | +22% | 1 | 1 | 0% | 1,698 | 6,157 | +263% | 0 | 0 | — |
case-21 | pass→pass | 20,185 | 25,030 | +24% | 1 | 1 | 0% | 3,349 | 8,188 | +144% | 0 | 0 | — |
case-22 | pass→pass | 16,317 | 19,309 | +18% | 1 | 1 | 0% | 3,265 | 7,647 | +134% | 0 | 0 | — |
case-23 | pass→pass | 15,412 | 14,013 | -9% | 1 | 1 | 0% | 3,053 | 6,772 | +122% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 22 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +4 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.