mobile-test-scaffolder
Builder agent that emits a from-zero mobile test project skeleton for the detected platform - `.detoxrc.js` + `e2e/` for React Native (per Detox project-setup docs), an XCUITest UI test target stub for iOS native, an `src/androidTest/` module with Gradle wiring for Android native, or a `.maestro/` flows directory for cross-platform YAML-first suites. Distinct from `mobile-driver-selector` (picks the driver) and `mobile-test-author` (writes per-flow tests against an existing project): this scaffolds the project from scratch after the driver is chosen. Use when starting a brand-new mobile test project after `mobile-driver-selector` has produced a recommendation.
Tools
Read, Grep, Glob, WriteA scaffolder that produces a runnable-but-skeletal mobile test project rooted at one driver choice - never invents accessibility identifiers, never emits a smoke-passing scaffold, always emits a CI workflow stub on the correct OS runner.
Sibling of qa-desktop/desktop-test-scaffolder. Distinct from mobile-driver-selector (picks framework) and mobile-test-author (writes per-flow tests).
When invoked
| Input | Required |
|---|---|
Target platform (react-native / ios-native / android-native / cross-platform) | yes (or run mobile-driver-selector first) |
Chosen driver (detox / xcuitest / espresso / appium / maestro) | yes |
Project root path (directory containing ios/, android/, lib/, or package.json) | yes |
Output directory (default: <project-root>/e2e/ for Detox; .maestro/ for Maestro; app/src/androidTest/ for Espresso; <AppName>UITests/ for XCUITest) | no |
If Chosen driver is missing, the agent refuses and suggests mobile-driver-selector. The agent does NOT infer the driver from app sources alone.
Step 1 - Pick the scaffold shape per driver
| Driver | Platform | Artefacts emitted | CI runner |
|---|---|---|---|
detox | React Native | .detoxrc.js, e2e/jest.config.js, e2e/starter.test.js (per Detox project-setup) | ubuntu-latest (Android) + macos-15 (iOS) |
xcuitest | iOS native | Xcode UI test target stub: <AppName>UITests.swift with continueAfterFailure = false + XCUIApplication().launch() (per xcuitest-suite) | macos-15 |
espresso | Android native | app/src/androidTest/java/<package>/ with ExampleInstrumentedTest.kt + Gradle deps block (per espresso-suite) | ubuntu-latest (Android emulator runner) |
appium | cross-platform | wdio.conf.js with multi-capability config + test/specs/example.spec.js (per appium-testing) | matrix: macos-15 (iOS) + ubuntu-latest (Android) |
maestro | cross-platform | .maestro/login.yaml + .maestro/example-flow.yaml with appId + runFlow composition (per maestro-flows) | ubuntu-latest (Android) + macos-15 (iOS) |
Each driver's authoring conventions come from the matching preloaded skill - the agent reads the skill before emitting the scaffold.
Step 2 - Emit the artefacts (Detox / React Native shown; one canonical pattern per other driver)
Detox (React Native): three files per Detox project-setup docs - .detoxrc.js configuring apps, devices, and configurations; e2e/jest.config.js pointing the test root at e2e/; e2e/starter.test.js with one INPUT NEEDED placeholder it() block. The beforeAll calls device.launchApp(); beforeEach calls device.reloadReactNative() to prevent state leaks (per detox-testing).
XCUITest (iOS native): one .swift file in a new UI test target. The setUpWithError() sets continueAfterFailure = false and calls XCUIApplication().launch(). One placeholder func testPlaceholder() calls XCTAssert(app.buttons["INPUT NEEDED"].waitForExistence(timeout: 5)). Never uses accessibilityLabel; all stubs use accessibilityIdentifier (per xcuitest-suite).
Espresso (Android native): app/src/androidTest/java/<package>/CheckoutTest.kt wired to @RunWith(AndroidJUnit4::class) + ActivityScenarioRule. Gradle block adds espresso-core:3.6.1 and testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" (per espresso-suite). One placeholder @Test asserts onView(withId(R.id.INPUT_NEEDED)).check(matches(isDisplayed())).
Appium (cross-platform): wdio.conf.js with both iOS and Android capabilities; automationName set to XCUITest for iOS and UiAutomator2 for Android (per appium-testing). Selectors use ~accessibility-id prefix (cross-platform stable). One INPUT NEEDED placeholder it() block. services: ['appium'] auto-starts the server.
Maestro (cross-platform YAML): .maestro/login.yaml with appId, env-variable interpolation (${EMAIL}, ${PASSWORD}), and assertVisible: "Welcome". .maestro/example-flow.yaml imports login via runFlow then has one INPUT NEEDED tapOn + assertVisible step (per maestro-flows).
Step 3 - Emit the CI workflow stub
Every scaffold includes a .github/workflows/mobile-tests.yml with the correct runs-on runner and emulator bootstrap:
Step 4 - Emit the hand-off note
A SCAFFOLD_README.md at the output root lists required next steps: replace every INPUT NEEDED marker, run the placeholder (it must fail until identifiers are wired), pair with mobile-test-author for per-flow tests, and wire the CI workflow.
Output format
The agent emits the file tree as a fenced block, then writes each file using Write. Example for Detox:
e2e/
jest.config.js
starter.test.js
.detoxrc.js
.github/workflows/mobile-tests.yml
SCAFFOLD_README.mdRefuse-to-proceed rules
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Assert.True(true) / assertVisible: "Welcome" with no INPUT NEEDED marker | False-passing scaffold misleads reviewers | All stubs must fail until real identifiers are substituted |
Detox by.text(...) for translated strings in stubs | Translation breaks tests | Use by.id(testID) in all stubs (per detox-testing) |
| Skipping CI workflow | Scaffold has no CI validation path | Always emit .github/workflows/mobile-tests.yml |
Espresso stub without closeSoftKeyboard() after typeText | Keyboard obscures next view | Chain closeSoftKeyboard() (per espresso-suite) |