diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fd8e268..eb4f77f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -3,8 +3,6 @@ name: "CodeQL" on: push: branches: ["main"] - pull_request: - branches: ["main"] schedule: - cron: "16 16 * * 0" diff --git a/appium/scripts/README.md b/appium/scripts/README.md index ab986a7..e9fb3de 100644 --- a/appium/scripts/README.md +++ b/appium/scripts/README.md @@ -1,6 +1,6 @@ # Local Test Runner -`run-local.sh` builds the app, boots a simulator/emulator, starts Appium, resets app state, and runs the E2E test suite. +`run-local.sh` builds the app, boots a simulator/emulator, starts Appium, resets app state, and runs the E2E test suite. With `--install-only` it stops after installing the app. ## Setup @@ -75,19 +75,21 @@ If `--platform` or `--sdk` are not provided, the script prompts interactively. ### Options -| Flag | Description | -| --------------- | ---------------------------------------------------------------------------------------------- | -| `--platform=P` | `ios` or `android` | -| `--sdk=S` | `flutter`, `react-native`, `cordova`, `capacitor`, `expo`, `dotnet`, `unity`, `android`, `ios` | -| `--spec=GLOB` | Spec file glob (default: `tests/specs/**/*.spec.ts`) | -| `--skip` | Skip build, device launch, and app reset (rerun tests only) | -| `--skip-build` | Skip app build (reuse existing `.app`/`.apk`) | -| `--skip-device` | Skip simulator/emulator launch | -| `--skip-reset` | Keep existing app data between runs | -| `--pods` | Use `examples/demo-pods` for Flutter, Cordova, and Capacitor | -| `--release` | Check out the latest release point in each SDK repo first | -| `--main` | Check out the latest `main` in each SDK repo first | -| `-h, --help` | Show help | +| Flag | Description | +| ---------------- | ---------------------------------------------------------------------------------------------- | +| `--platform=P` | `ios` or `android` | +| `--sdk=S` | `flutter`, `react-native`, `cordova`, `capacitor`, `expo`, `dotnet`, `unity`, `android`, `ios` | +| `--spec=GLOB` | Spec file glob (default: `tests/specs/**/*.spec.ts`) | +| `--skip` | Skip build, device launch, and app reset (rerun tests only) | +| `--skip-build` | Skip app build (reuse existing `.app`/`.apk`) | +| `--skip-device` | Skip simulator/emulator launch | +| `--skip-reset` | Keep existing app data between runs | +| `--install-only` | Build, launch the device, reset, and install the app; skip Appium and tests | +| `--pick-device` | Prompt for which iOS simulator or Android emulator to use | +| `--pods` | Use `examples/demo-pods` for Flutter, Cordova, and Capacitor | +| `--release` | Check out the latest release point in each SDK repo first | +| `--main` | Check out the latest `main` in each SDK repo first | +| `-h, --help` | Show help | ### Examples @@ -144,6 +146,8 @@ Skip only the build (simulator + reset still happen): ./run-all.sh --release # check out the latest release point per repo first ./run-all.sh --main # check out the latest main per repo first ./run-all.sh --bail # stop after the first failing combo +./run-all.sh --pick-device # prompt for a simulator and/or emulator up front +./run-all.sh --sdks=flutter --install-only # build and install on both platforms, no tests ``` `--release` is available on both `run-all.sh` and `run-local.sh`. It runs `checkout-releases.sh`, which checks out the newest stable `rel/X.Y.Z` branch (for expo/ios, the newest semver tag unless a newer stable `rel/X.Y.Z` branch exists for a pending release) in each SDK repo, honoring the `*_DIR` overrides from `.env`. Uncommitted changes, including untracked files, are stashed before checkout and can be restored later with `git stash pop`. diff --git a/appium/scripts/run-all.sh b/appium/scripts/run-all.sh index 2695d1b..450ae1b 100755 --- a/appium/scripts/run-all.sh +++ b/appium/scripts/run-all.sh @@ -11,6 +11,7 @@ if [[ -f "$SCRIPT_DIR/.env" ]]; then fi source "$SCRIPT_DIR/run-local/repo-dir.sh" +source "$SCRIPT_DIR/run-local/device-picker.sh" # ── Colors / logging ───────────────────────────────────────────────────────── RED='\033[0;31m' @@ -34,10 +35,15 @@ PODS_REQUESTED=0 RELEASE=0 MAIN=0 WIPE_EMULATOR=0 +PICK_DEVICE=0 +INSTALL_ONLY=0 for arg in "$@"; do case "$arg" in --skip-build|--skip-device|--skip-reset|--skip|--quiet|-q) EXTRA_ARGS+=("$arg") ;; + --install-only) + INSTALL_ONLY=1 + EXTRA_ARGS+=("$arg") ;; --wipe-emulator) WIPE_EMULATOR=1 ;; --pods) @@ -57,6 +63,8 @@ for arg in "$@"; do SDKS_FILTER="${arg#*=}" ;; --bail) BAIL=1 ;; + --pick-device) + PICK_DEVICE=1 ;; --help|-h) cat < 1 )); then + error "--install-only would install ${platform_sdks[*]} on $platform, but every demo shares one bundle id so only the last would remain. Narrow with --sdks= or --platform=." + exit 2 + fi + done +fi + # --release/--main switch the default checkout to a fixed ref, so there is no # worktree to choose. if (( RELEASE || MAIN )); then @@ -166,6 +197,24 @@ for sdk in ${RUN_SDKS[@]+"${RUN_SDKS[@]}"}; do resolve_sdk_dir "$sdk" done +IOS_DEVICE_ARGS=() +ANDROID_DEVICE_ARGS=() +if (( PICK_DEVICE )); then + for platform in "${PLATFORMS[@]}"; do + has_combo=0 + for sdk in ${RUN_SDKS[@]+"${RUN_SDKS[@]}"}; do + combo_runs "$sdk" "$platform" && { has_combo=1; break; } + done + (( has_combo )) || continue + pick_device "$platform" || exit 2 + if [[ "$platform" == "ios" ]]; then + IOS_DEVICE_ARGS=(--device="$PICKED_DEVICE" --udid="$PICKED_UDID") + else + ANDROID_DEVICE_ARGS=(--device="$PICKED_DEVICE") + fi + done +fi + if (( RELEASE && ${#RUN_SDKS[@]} > 0 )); then echo -e "${BOLD}━━━ Checking out latest releases ━━━${NC}" if ! "$SCRIPT_DIR/checkout-releases.sh" "${RUN_SDKS[@]}"; then @@ -201,17 +250,9 @@ for platform in "${PLATFORMS[@]}"; do else label="${sdk} / ${platform}" fi - if [[ "$sdk" == "android" && "$platform" == "ios" ]]; then + if ! combo_runs "$sdk" "$platform"; then if [[ -n "$PLATFORM_FILTER" ]]; then - warn "--sdk=android only runs on --platform=android; skipping --platform=ios" - RESULTS+=("SKIP ${label}") - SKIPPED=$((SKIPPED + 1)) - fi - continue - fi - if [[ "$sdk" == "ios" && "$platform" == "android" ]]; then - if [[ -n "$PLATFORM_FILTER" ]]; then - warn "--sdk=ios only runs on --platform=ios; skipping --platform=android" + warn "--sdk=$sdk only runs on --platform=$sdk; skipping --platform=$platform" RESULTS+=("SKIP ${label}") SKIPPED=$((SKIPPED + 1)) fi @@ -229,6 +270,11 @@ for platform in "${PLATFORMS[@]}"; do flutter|cordova|capacitor) combo_args+=(--pods) ;; esac fi + if [[ "$platform" == "ios" ]]; then + combo_args+=(${IOS_DEVICE_ARGS[@]+"${IOS_DEVICE_ARGS[@]}"}) + else + combo_args+=(${ANDROID_DEVICE_ARGS[@]+"${ANDROID_DEVICE_ARGS[@]}"}) + fi if [[ "$platform" == "android" ]] && (( WIPE_EMULATOR )) && (( ! ANDROID_EMULATOR_WIPED )); then combo_args+=(--wipe-emulator) ANDROID_EMULATOR_WIPED=1 diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index e63ef42..dc1391c 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -16,6 +16,7 @@ RUN_LOCAL_LIB_DIR="$SCRIPT_DIR/run-local" source "$RUN_LOCAL_LIB_DIR/common.sh" source "$RUN_LOCAL_LIB_DIR/repo-dir.sh" source "$RUN_LOCAL_LIB_DIR/config.sh" +source "$RUN_LOCAL_LIB_DIR/device-picker.sh" source "$RUN_LOCAL_LIB_DIR/sdk-artifacts.sh" source "$RUN_LOCAL_LIB_DIR/build.sh" source "$RUN_LOCAL_LIB_DIR/runtime.sh" @@ -84,6 +85,13 @@ main() { begin_quiet_setup build_app + if [[ "$INSTALL_ONLY" == true ]]; then + prepare_install_only + end_quiet_setup + echo "" + info "=== Installed (--install-only, tests skipped) ===" + return + fi prepare_runtime install_test_deps end_quiet_setup diff --git a/appium/scripts/run-local/config.sh b/appium/scripts/run-local/config.sh index b7200fe..6dab8a1 100644 --- a/appium/scripts/run-local/config.sh +++ b/appium/scripts/run-local/config.sh @@ -8,6 +8,10 @@ configure_runner() { SKIP_BUILD=false SKIP_DEVICE=false SKIP_RESET=false + INSTALL_ONLY=false + PICK_DEVICE=false + DEVICE_FLAG=false + UDID_FLAG=false SPEC="" QUIET=false PODS_DEMO=false @@ -23,9 +27,10 @@ configure_runner() { case "$arg" in --platform=*) PLATFORM="${arg#--platform=}" ;; --sdk=*) SDK_TYPE="${arg#--sdk=}" ;; - --device=*) DEVICE="${arg#--device=}" ;; + --device=*) DEVICE="${arg#--device=}"; DEVICE_FLAG=true ;; + --pick-device) PICK_DEVICE=true ;; --device-real) IOS_REAL_DEVICE=true; SKIP_DEVICE=true ;; - --udid=*) UDID="${arg#--udid=}" ;; + --udid=*) UDID="${arg#--udid=}"; UDID_FLAG=true ;; --appium-port=*) APPIUM_PORT="${arg#--appium-port=}" ;; --wda-local-port=*) WDA_LOCAL_PORT="${arg#--wda-local-port=}" ;; --system-port=*) SYSTEM_PORT="${arg#--system-port=}" ;; @@ -33,6 +38,7 @@ configure_runner() { --skip-build) SKIP_BUILD=true ;; --skip-device) SKIP_DEVICE=true ;; --skip-reset) SKIP_RESET=true ;; + --install-only) INSTALL_ONLY=true ;; --wipe-emulator) WIPE_EMULATOR=true ;; --pods) PODS_DEMO=true ;; --release) RELEASE=true ;; @@ -57,6 +63,7 @@ Options: ios = native iOS (OneSignal-iOS-SDK/examples/demo); skips with exit 0 when --platform=android. --device=NAME Device/simulator/AVD name (default: iPhone 17 / Android 17) + --pick-device Prompt for which iOS simulator or Android emulator to use --appium-port=N Appium server port (default: 4723). Use unique values when running multiple sessions in parallel on the same host. --system-port=N UiAutomator2 systemPort (Android only). Required when @@ -66,6 +73,8 @@ Options: --skip-build Skip app build (reuse existing) --skip-device Skip simulator/emulator launch --skip-reset Keep existing app data + --install-only Build, launch the device, reset, and install the app, + then exit without starting Appium or running tests --wipe-emulator Wipe the Android AVD before launch. Deletes all emulator apps and data; incompatible with --skip-device. --pods Use examples/demo-pods instead of examples/demo for @@ -128,6 +137,16 @@ USAGE esac done + # A CLI device beats device settings from .env/env. Without a pinned runtime, + # start_ios_simulator matches the name on the newest runtime that has it. + if [[ "$DEVICE_FLAG" == true ]]; then + AVD_NAME="" + IOS_SIMULATOR="" + IOS_RUNTIME="" + IOS_RUNTIME_ANY=true + [[ "$UDID_FLAG" == true ]] || UDID="" + fi + # Ensure values set via CLI flags propagate to wdio (which reads them as env). export APPIUM_PORT [[ -n "$WDA_LOCAL_PORT" ]] && export WDA_LOCAL_PORT @@ -176,6 +195,15 @@ USAGE error "--release and --main are mutually exclusive" fi + if [[ "$PICK_DEVICE" == true ]]; then + [[ "$IOS_REAL_DEVICE" == false ]] || error "--pick-device cannot be combined with --device-real" + pick_device "$PLATFORM" || exit 1 + DEVICE="$PICKED_DEVICE" + AVD_NAME="" + IOS_SIMULATOR="" + [[ "$PLATFORM" == "ios" ]] && UDID="$PICKED_UDID" + fi + resolve_sdk_dir "$SDK_TYPE" if [[ "$RELEASE" == true ]]; then @@ -198,6 +226,19 @@ USAGE # calls `vpx wdio run` directly on BrowserStack — so these checks are # local-only by construction. preflight() { + if [[ "$INSTALL_ONLY" != true ]]; then + preflight_test_tooling + fi + + if [[ -z "${ONESIGNAL_APP_ID:-}" || -z "${ONESIGNAL_API_KEY:-}" ]]; then + error "ONESIGNAL_APP_ID / ONESIGNAL_API_KEY not set. Use the OneSignal app + dedicated to Appium tests (not a general/shared app — its live in-app + marketing campaigns can cover the UI and cause misleading 'element not + displayed' failures). Set both in $SCRIPT_DIR/.env (cp .env.example .env)." + fi + } + + preflight_test_tooling() { command -v appium >/dev/null 2>&1 \ || error "appium not found on PATH. Install it with: npm i -g appium" @@ -239,13 +280,6 @@ USAGE export WDIO_USE_NATIVE_FETCH=1 info "Node $node_major detected — setting WDIO_USE_NATIVE_FETCH=1 (works around webdriverio's undici dispatcher being rejected by Node 26+ fetch)." fi - - if [[ -z "${ONESIGNAL_APP_ID:-}" || -z "${ONESIGNAL_API_KEY:-}" ]]; then - error "ONESIGNAL_APP_ID / ONESIGNAL_API_KEY not set. Use the OneSignal app - dedicated to Appium tests (not a general/shared app — its live in-app - marketing campaigns can cover the UI and cause misleading 'element not - displayed' failures). Set both in $SCRIPT_DIR/.env (cp .env.example .env)." - fi } preflight @@ -423,7 +457,7 @@ USAGE DEVICE="${DEVICE:-iPhone 17}" OS_VERSION="${OS_VERSION:-26.5}" IOS_SIMULATOR="${IOS_SIMULATOR:-$DEVICE}" - IOS_RUNTIME="${IOS_RUNTIME:-iOS-26-5}" + [[ "${IOS_RUNTIME_ANY:-false}" == true ]] || IOS_RUNTIME="${IOS_RUNTIME:-iOS-26-5}" else DEVICE="${DEVICE:-Android 17}" OS_VERSION="${OS_VERSION:-17}" diff --git a/appium/scripts/run-local/device-picker.sh b/appium/scripts/run-local/device-picker.sh new file mode 100644 index 0000000..32c3ff6 --- /dev/null +++ b/appium/scripts/run-local/device-picker.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +# Prompts for an iOS simulator or Android AVD. Sets PICKED_DEVICE (simulator +# or AVD name) and, for iOS, PICKED_UDID, since simulator names repeat across +# runtimes. +pick_device() { + local platform="$1" + local -a names=() udids=() labels=() + local name udid label + PICKED_DEVICE="" + PICKED_UDID="" + + if [[ "$platform" == "ios" ]]; then + while IFS='|' read -r udid name label; do + [[ -n "$udid" ]] || continue + udids+=("$udid") + names+=("$name") + labels+=("$label") + done < <(xcrun simctl list devices available -j 2>/dev/null | python3 -c ' +import json, sys +data = json.load(sys.stdin) +rows = [] +for runtime, devices in data["devices"].items(): + rt = runtime.rsplit(".", 1)[-1] + if not rt.startswith("iOS-"): + continue + try: + ver = tuple(int(p) for p in rt[4:].split("-")) + except ValueError: + continue + for d in devices: + label = d["name"] + " (iOS " + ".".join(map(str, ver)) + ")" + if d["state"] == "Booted": + label += " [booted]" + rows.append((ver, d["name"], d["udid"], label)) +for ver, name, udid, label in sorted(rows, key=lambda r: (tuple(-v for v in r[0]), not r[1].startswith("iPhone"), r[1])): + print(udid + "|" + name + "|" + label) +' 2>/dev/null) + else + local running + running=$(ps -axo command= 2>/dev/null | grep -oE -- '-avd [^ ]+' | awk '{print $2}' || true) + while read -r name; do + [[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || continue + label="$name" + grep -qxF -- "$name" <<<"$running" && label+=" [running]" + names+=("$name") + labels+=("$label") + done < <(emulator -list-avds 2>/dev/null) + fi + + if (( ${#names[@]} == 0 )); then + if [[ "$platform" == "ios" ]]; then + echo "No iOS simulators found (xcrun simctl list devices available)" >&2 + else + echo "No Android AVDs found (emulator -list-avds)" >&2 + fi + return 1 + fi + + echo "" + if [[ "$platform" == "ios" ]]; then + echo -e "${GREEN}Select iOS simulator:${NC}" + else + echo -e "${GREEN}Select Android emulator:${NC}" + fi + local i + for i in "${!labels[@]}"; do + echo " $((i + 1))) ${labels[$i]}" + done + + local choice + while true; do + if ! read -rp "> " choice; then + echo "" >&2 + echo "No input available; cannot pick a device" >&2 + return 1 + fi + if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#names[@]} )); then + PICKED_DEVICE="${names[$((choice - 1))]}" + [[ "$platform" == "ios" ]] && PICKED_UDID="${udids[$((choice - 1))]}" + return 0 + fi + echo " Invalid choice. Enter a number 1-${#names[@]}." + done +} diff --git a/appium/scripts/run-local/runtime.sh b/appium/scripts/run-local/runtime.sh index 4d86a4c..d82ea02 100644 --- a/appium/scripts/run-local/runtime.sh +++ b/appium/scripts/run-local/runtime.sh @@ -23,23 +23,30 @@ start_ios_simulator() { fi local udid + # An empty IOS_RUNTIME matches any runtime; newest wins. udid=$(xcrun simctl list devices available -j \ - | python3 -c " + | python3 -c ' import json, sys +runtime_filter, name = sys.argv[1], sys.argv[2] data = json.load(sys.stdin) -for runtime, devices in data['devices'].items(): - if '$IOS_RUNTIME' in runtime: - for d in devices: - if d['name'] == '$IOS_SIMULATOR' and d['isAvailable']: - print(d['udid']); sys.exit(0) -" 2>/dev/null || true) +def version(runtime): + try: + return tuple(int(p) for p in runtime.rsplit(".", 1)[-1].replace("iOS-", "").split("-")) + except ValueError: + return () +for runtime in sorted(data["devices"], key=version, reverse=True): + if runtime_filter in runtime: + for d in data["devices"][runtime]: + if d["name"] == name and d["isAvailable"]: + print(d["udid"]); sys.exit(0) +' "$IOS_RUNTIME" "$IOS_SIMULATOR" 2>/dev/null || true) # Requested device/runtime isn't installed on this machine (e.g. defaults # assume iOS 26.5 but only an older runtime is installed). Fall back to the booted # simulator if there is one, else the newest installed iOS runtime, and # align DEVICE/OS_VERSION so the Appium session targets what actually runs. if [[ -z "$udid" ]]; then - warn "Simulator '$IOS_SIMULATOR' ($IOS_RUNTIME) not found on this machine." + warn "Simulator '$IOS_SIMULATOR' (${IOS_RUNTIME:-any runtime}) not found on this machine." local fallback fallback=$(xcrun simctl list devices -j \ | python3 -c " @@ -72,12 +79,12 @@ for runtime, devices in data['devices'].items(): if avail: runtimes.append((ver, rt, avail)) for ver, rt, avail in sorted(runtimes, reverse=True): - exact = [d for d in avail if d['name'] == '$IOS_SIMULATOR'] + exact = [d for d in avail if d['name'] == sys.argv[1]] iphones = sorted((d for d in avail if d['name'].startswith('iPhone')), key=lambda d: d['name']) pick = exact[0] if exact else (iphones[-1] if iphones else avail[0]) print(pick['udid'] + '|' + pick['name'] + '|' + rt + '|' + '.'.join(str(p) for p in ver)) sys.exit(0) -" 2>/dev/null || true) +" "$IOS_SIMULATOR" 2>/dev/null || true) fi if [[ -z "$fallback" ]]; then error "No usable iOS simulator found. Run: xcrun simctl list devices available, then set DEVICE / OS_VERSION / IOS_RUNTIME in $SCRIPT_DIR/.env" @@ -263,6 +270,7 @@ start_device() { || error "Android emulator '$AVD_NAME' is not running; remove --skip-device or start it first" pin_android_emulator "$emulator_udid" fi + [[ "$PLATFORM" == "ios" ]] && resolve_booted_simulator info "Skipping device launch (--skip-device)" return fi @@ -390,12 +398,95 @@ validate_existing_app() { fi } +ios_sim_version() { + xcrun simctl list devices -j 2>/dev/null | python3 -c ' +import json, sys +for runtime, devices in json.load(sys.stdin)["devices"].items(): + if any(d["udid"] == sys.argv[1] for d in devices): + print(runtime.rsplit(".", 1)[-1].replace("iOS-", "").replace("-", ".")) +' "$1" 2>/dev/null || true +} + +# With --skip-device, pin the already-booted simulator so reset and install +# don't hit simctl's ambiguous `booted` target. Prefers a name match. +resolve_booted_simulator() { + [[ "$IOS_REAL_DEVICE" == false && -z "$UDID" ]] || return 0 + local -a booted=() + local udid + while read -r udid; do + [[ -n "$udid" ]] && booted+=("$udid") + done < <(xcrun simctl list devices booted -j 2>/dev/null | python3 -c ' +import json, sys +booted = [d for ds in json.load(sys.stdin)["devices"].values() for d in ds if d["state"] == "Booted"] +named = [d for d in booted if d["name"] == sys.argv[1]] +for d in named or booted: + print(d["udid"]) +' "${IOS_SIMULATOR:-$DEVICE}" 2>/dev/null) + + if (( ${#booted[@]} == 1 )); then + UDID="${booted[0]}" + info "Using booted simulator $UDID" + elif [[ "$INSTALL_ONLY" == true ]]; then + if (( ${#booted[@]} == 0 )); then + error "No booted simulator; remove --skip-device or boot one first" + fi + error "Multiple simulators booted; pass --udid or --pick-device to choose one" + fi +} + +# Appium matches platformVersion against the running device, so a picked or +# --device/--udid target must not keep the default OS_VERSION. +sync_device_os_version() { + local version="" + if [[ "$PLATFORM" == "android" ]]; then + [[ -n "${ANDROID_UDID:-}" ]] || return 0 + version=$(adb_with_timeout 10 -s "$ANDROID_UDID" shell getprop ro.build.version.release 2>/dev/null | tr -d '\r' || true) + else + [[ "$IOS_REAL_DEVICE" == false && -n "$UDID" ]] || return 0 + version=$(ios_sim_version "$UDID") + fi + if [[ ! "$version" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then + warn "Could not read the device OS version; keeping OS_VERSION=$OS_VERSION (Appium may reject a mismatched platformVersion)" + return 0 + fi + [[ "$version" != "$OS_VERSION" ]] || return 0 + info "Using OS_VERSION=$version to match the running device (was $OS_VERSION)" + OS_VERSION="$version" +} + prepare_runtime() { validate_existing_app start_device + sync_device_os_version check_android_storage install_chromedriver_if_needed start_appium cleanup_android_automation reset_app } + +# Appium installs the app at session start during test runs, so install-only +# has to do it explicitly. No -g on adb: tests expect the permission prompt. +install_app() { + info "Installing $APP_PATH..." + if [[ "$PLATFORM" == "android" ]]; then + adb -s "$ANDROID_UDID" install -r "$APP_PATH" >/dev/null \ + || error "Failed to install $APP_PATH on $ANDROID_UDID" + elif [[ "$IOS_REAL_DEVICE" == true ]]; then + xcrun devicectl device install app --device "$UDID" "$APP_PATH" >/dev/null \ + || error "Failed to install $APP_PATH on device $UDID" + else + [[ -n "$UDID" ]] || error "No simulator selected for install; pass --udid or --pick-device" + xcrun simctl install "$UDID" "$APP_PATH" \ + || error "Failed to install $APP_PATH on simulator $UDID" + fi + info "Installed $BUNDLE_ID" +} + +prepare_install_only() { + validate_existing_app + start_device + check_android_storage + reset_app + install_app +}