Self-test a mobile feature change or bug fix after implementation in `apps/mobile`. Use this whenever the user asks to verify a mobile change, run simulator acceptance, smoke-test a mobile PR, or provide screenshot proof for a mobile fix. This skill decides between prod vs local API mode, starts the local follow-server when needed, builds a release app, uses Maestro only to bootstrap registration for non-auth work, then switches to screenshot-driven visual validation and returns screenshot evidence.
Validate a mobile change after implementation.
This skill extends ../mobile-e2e/SKILL.md. Read that skill first for the baseline doctor checks, iOS simulator boot rules, Java/Android SDK setup, and Maestro artifact conventions. Then apply the extra rules in this skill.
../mobile-e2e/SKILL.mdapps/mobile/e2e/run-maestro.shapps/mobile/e2e/flows/ios/register.yamlapps/mobile/e2e/flows/android/register.yamlapps/mobile/e2e/flows/shared/*.yaml/Users/diygod/.agents/skills/axe/SKILL.mdapps/mobile/app.config.tsapps/mobile/eas.jsonapps/mobile/e2e/artifacts//Users/diygod/Code/Projects/follow-server/Users/diygod/Code/Projects/follow-server.EXPO_PUBLIC_E2E_LANGUAGE=en unless the user explicitly wants another language. The existing Maestro flows assume English UI.This section overrides the shared device-selection guidance from ../mobile-e2e/SKILL.md.
Self-test runs must be isolated because other agents may be using simulators or emulators on the same machine.
booted, an already-running simulator, or a generic Android serial such as emulator-5554.Use this decision order:
prod or local, obey that.local.prod.Map the chosen mode into the release build:
prod mode: EXPO_PUBLIC_E2E_ENV_PROFILE=prodlocal mode: EXPO_PUBLIC_E2E_ENV_PROFILE=localDo not silently reuse a build from the other mode. Rebuild the release app when switching between prod and local.
From repo root:
cd apps/mobile
pnpm run e2e:doctor
pnpm run typecheck
If these fail, stop and report the blocker before attempting simulator work.
local mode requires the local server to be available at http://localhost:3000.
Before starting anything, check whether it is already running. Do not start a duplicate server.
FOLLOW_SERVER_LOG=/tmp/follow-server-dev-core.log
if pgrep -af "pnpm dev:core" >/dev/null 2>&1 || lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1; then
echo "follow-server already running"
else
(
cd /Users/diygod/Code/Projects/follow-server
nohup pnpm dev:core >"$FOLLOW_SERVER_LOG" 2>&1 &
)
fi
for _ in $(seq 1 60); do
nc -z 127.0.0.1 3000 >/dev/null 2>&1 && break
sleep 2
done
nc -z 127.0.0.1 3000 >/dev/null 2>&1
If the task depends on other local surfaces such as http://localhost:2233, call that out explicitly instead of pretending the mobile test fully covers it.
Use release-style builds so the test matches user-facing behavior.
PROFILE=e2e-ios-simulatorPROFILE=e2e-androidAlways pair those with the chosen API mode and language:
export EXPO_PUBLIC_E2E_ENV_PROFILE=<prod-or-local>
export EXPO_PUBLIC_E2E_LANGUAGE=en
Do not attach to an existing simulator from ../mobile-e2e/SKILL.md. Create a dedicated temporary simulator for this run and keep using only its UDID.
Pick the latest available iOS runtime and a recent iPhone device type, then create a temporary simulator.
IOS_SIM_NAME="CodexSelfTest-$(date +%Y%m%d-%H%M%S)"
IOS_RUNTIME_ID="<latest available iOS runtime identifier from `xcrun simctl list runtimes`>"
IOS_DEVICE_TYPE_ID="<recent iPhone device type identifier from `xcrun simctl list devicetypes`>"
IOS_UDID="$(xcrun simctl create "$IOS_SIM_NAME" "$IOS_DEVICE_TYPE_ID" "$IOS_RUNTIME_ID")"
cleanup_ios_simulator() {
xcrun simctl shutdown "$IOS_UDID" >/dev/null 2>&1 || true
xcrun simctl delete "$IOS_UDID" >/dev/null 2>&1 || true
}
trap cleanup_ios_simulator EXIT
Do not switch to another simulator after IOS_UDID is created.
xcrun simctl boot "$IOS_UDID"
xcrun simctl bootstatus "$IOS_UDID" -b
open -a Simulator --args -CurrentDeviceUDID "$IOS_UDID"
If other simulators are already booted, leave them alone and continue using only IOS_UDID.
cd apps/mobile/ios
pod install
PROFILE=e2e-ios-simulator \
EXPO_PUBLIC_E2E_ENV_PROFILE=<prod-or-local> \
EXPO_PUBLIC_E2E_LANGUAGE=en \
xcodebuild -workspace Folo.xcworkspace \
-scheme Folo \
-configuration Release \
-sdk iphonesimulator \
-destination "id=$IOS_UDID" \
clean build
On Apple Silicon Macs, when the build is only for the dedicated simulator created for the current self-test run, prefer compiling only the active arm64 simulator architecture:
ONLY_ACTIVE_ARCH=YES \
ARCHS=arm64
Do not use that optimization when you need a universal simulator bundle for other machines or when the host Mac is Intel.
Expected output pattern:
~/Library/Developer/Xcode/DerivedData/.../Build/Products/Release-iphonesimulator/Folo.app
xcrun simctl install "$IOS_UDID" <PATH_TO_Folo.app>
xcrun simctl launch "$IOS_UDID" is.follow
After the app is running on the dedicated iOS simulator, use the $axe skill for simulator interaction during visual validation. Use $axe as the default interaction layer for iOS screenshot-driven checks.
Reuse the Java and Android SDK setup from ../mobile-e2e/SKILL.md.
Do not attach to a shared emulator. Create a dedicated temporary AVD for this run and keep using only its recorded serial.
Create a fresh AVD backed by an installed phone system image.
ANDROID_AVD_NAME="codex-self-test-$(date +%Y%m%d-%H%M%S)"
ANDROID_AVD_PACKAGE="<installed Android system image package>"
ANDROID_AVD_DEVICE="<phone hardware profile>"
avdmanager create avd -n "$ANDROID_AVD_NAME" -k "$ANDROID_AVD_PACKAGE" -d "$ANDROID_AVD_DEVICE" --force
ANDROID_EMULATOR_PORT=""
for port in 5554 5556 5558 5560 5562 5564; do
if ! lsof -nP -iTCP:$port >/dev/null 2>&1 && ! lsof -nP -iTCP:$((port + 1)) >/dev/null 2>&1; then
ANDROID_EMULATOR_PORT="$port"
break
fi
done
[ -n "$ANDROID_EMULATOR_PORT" ] || {
echo "No free Android emulator port found"
exit 1
}
ANDROID_DEVICE_ID="emulator-$ANDROID_EMULATOR_PORT"
cleanup_android_emulator() {
adb -s "$ANDROID_DEVICE_ID" emu kill >/dev/null 2>&1 || true
avdmanager delete avd -n "$ANDROID_AVD_NAME" >/dev/null 2>&1 || true
}
trap cleanup_android_emulator EXIT
emulator @"$ANDROID_AVD_NAME" -port "$ANDROID_EMULATOR_PORT" -no-snapshot -wipe-data &
adb -s "$ANDROID_DEVICE_ID" wait-for-device
If other emulators are already booted, ignore them and continue using only ANDROID_DEVICE_ID.
If apps/mobile/android does not exist locally, generate it first.
cd apps/mobile
pnpm expo prebuild android
cd apps/mobile/android
PROFILE=e2e-android \
EXPO_PUBLIC_E2E_ENV_PROFILE=<prod-or-local> \
EXPO_PUBLIC_E2E_LANGUAGE=en \
./gradlew clean app:assembleRelease --console=plain
Expected APK path:
apps/mobile/android/app/build/outputs/apk/release/app-release.apk
adb -s "$ANDROID_DEVICE_ID" install -r apps/mobile/android/app/build/outputs/apk/release/app-release.apk
adb -s "$ANDROID_DEVICE_ID" shell monkey -p is.follow -c android.intent.category.LAUNCHER 1
Delete the temporary simulator or emulator created for the run before returning control to the user.
xcrun simctl shutdown "$IOS_UDID" >/dev/null 2>&1 || true
xcrun simctl delete "$IOS_UDID" >/dev/null 2>&1 || true
adb -s "$ANDROID_DEVICE_ID" emu kill >/dev/null 2>&1 || true
avdmanager delete avd -n "$ANDROID_AVD_NAME" >/dev/null 2>&1 || true
Do not leave temporary devices behind for other agents.
This is the core difference from mobile-e2e.
Use the existing automated registration flow first to bootstrap a clean logged-in account, then do the real verification visually.
Examples:
Generate a unique test account before running the flow:
export E2E_PASSWORD='Password123!'
export E2E_EMAIL="folo-self-test-$(date +%Y%m%d%H%M%S)@example.com"
For non-auth iOS self-tests, bootstrap auth through the standard iOS runner mode after the app has been installed and launched once:
cd apps/mobile
pnpm run e2e:ios:bootstrap
This bootstrap path is the default for prod and local self-tests. Only skip it when the feature under test is login, registration, sign-out, session restoration, or another auth-specific flow that must be validated visually end-to-end.
cd apps/mobile
maestro test --format junit --platform ios --device "$IOS_UDID" \
--debug-output e2e/artifacts/ios/register-bootstrap \
-e E2E_EMAIL="$E2E_EMAIL" \
-e E2E_PASSWORD="$E2E_PASSWORD" \
e2e/flows/ios/register.yaml
cd apps/mobile
maestro test --format junit --platform android --device "$ANDROID_DEVICE_ID" \
--debug-output e2e/artifacts/android/register-bootstrap \
-e E2E_EMAIL="$E2E_EMAIL" \
-e E2E_PASSWORD="$E2E_PASSWORD" \
e2e/flows/android/register.yaml
After registration succeeds, continue with screenshot-driven visual testing.
Do not rely on the existing Maestro auth flows for the actual verification. Use a fully visual/manual run instead so the changed UX itself is what gets tested.
Examples:
For auth-related work:
Once the app is in the right state, drive the rest of the validation visually. On iOS, use the $axe skill as the default interaction tool for this phase. On Android, use the interaction tooling available in the current environment. Screenshots are the source of truth for acceptance.
Create a timestamped artifact folder first:
REPO_ROOT="$(git rev-parse --show-toplevel)"
ARTIFACT_DIR="$REPO_ROOT/apps/mobile/e2e/artifacts/manual/$(date +%Y%m%d-%H%M%S)-<platform>-<prod-or-local>"
mkdir -p "$ARTIFACT_DIR"
Capture screenshots after each meaningful checkpoint.
xcrun simctl io "$IOS_UDID" screenshot "$ARTIFACT_DIR/<name>.png"
adb -s "$ANDROID_DEVICE_ID" exec-out screencap -p > "$ARTIFACT_DIR/<name>.png"
Minimum screenshot set for a complete self-test:
Add more screenshots when the flow has multiple important states.
Do not report success without screenshot evidence.
Use the screenshots to confirm at least these points when relevant:
prod or local)If the UI or behavior is ambiguous, capture another screenshot instead of guessing.
The final response must include:
If the client supports local image rendering, attach the key screenshots as images in the final message. Otherwise, list the absolute paths clearly so the user can open them.
local mode cannot reach the local server, do not silently fall back to prod.$axe is unavailable or unusable, report that limitation clearly and still return the screenshots you captured.