From 48d6a77a5b154452f700b45fe8557ec871e4d03e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Sun, 24 May 2026 12:26:06 -0700 Subject: [PATCH 01/11] feat(appium): add native iOS SDK support --- appium/scripts/run-all.sh | 20 +++++++--- appium/scripts/run-local.sh | 78 +++++++++++++++++++++++++++++++++---- demo/build.md | 2 +- 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/appium/scripts/run-all.sh b/appium/scripts/run-all.sh index 6854599..e1da0bc 100755 --- a/appium/scripts/run-all.sh +++ b/appium/scripts/run-all.sh @@ -15,7 +15,7 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*"; } # ── Args (forwarded as-is to run-local.sh) ──────────────────────────────────── -ALL_SDKS=(android cordova capacitor dotnet expo flutter react-native unity) +ALL_SDKS=(android cordova capacitor dotnet expo flutter ios react-native unity) EXTRA_ARGS=() PLATFORM_FILTER="" @@ -42,14 +42,16 @@ Usage: $0 [OPTIONS] Runs the Appium E2E suite across every SDK/platform combo by delegating to run-local.sh. Combos: cordova, capacitor, react-native, flutter, dotnet, -expo, unity on ios + android, plus android (native) on android only. +expo, unity on ios + android, plus android (native) on android only and +ios (native) on ios only. Options: --platform=ios|android Only run combos for the given platform (default: both) --sdks=LIST Comma-separated SDKs to run (default: all) Valid: cordova, capacitor, react-native, flutter, - dotnet, expo, unity, android - Note: 'android' (native) skips --platform=ios. + dotnet, expo, unity, android, ios + Note: 'android' (native) skips --platform=ios and + 'ios' (native) skips --platform=android. --bail Stop after the first failing combo Options forwarded to run-local.sh: @@ -98,7 +100,7 @@ SKIPPED=0 for platform in "${PLATFORMS[@]}"; do for sdk in "${SDKS[@]}"; do - # Native Android demo only exists for Android. + # Native demos only target their own platform. if [[ "$sdk" == "android" && "$platform" == "ios" ]]; then if [[ -n "$PLATFORM_FILTER" ]]; then warn "--sdk=android only runs on --platform=android; skipping --platform=ios" @@ -107,6 +109,14 @@ for platform in "${PLATFORMS[@]}"; do 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" + RESULTS+=("SKIP ${sdk} / ${platform}") + SKIPPED=$((SKIPPED + 1)) + fi + continue + fi label="${sdk} / ${platform}" echo "" echo -e "${BOLD}━━━ Running: ${label} ━━━${NC}" diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index 150735e..eef983d 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -64,9 +64,11 @@ via flags or env vars. Options: --platform=P ios | android - --sdk=S flutter | react-native | cordova | capacitor | dotnet | expo | unity | android + --sdk=S flutter | react-native | cordova | capacitor | dotnet | expo | unity | android | ios android = native Android (OneSignal-Android-SDK/examples/demo); skips with exit 0 when --platform=ios. + ios = native iOS (OneSignal-iOS-SDK/examples/demo); + skips with exit 0 when --platform=android. --device=NAME Device/simulator/AVD name (default: iPhone 17 / Samsung Galaxy S26) --appium-port=N Appium server port (default: 4723). Use unique values when running multiple sessions in parallel on the same host. @@ -106,6 +108,11 @@ Env vars (set in .env or export): ANDROID_DIR Native Android SDK repo root (default: ../../OneSignal-Android-SDK) ANDROID_FLAVOR Native Android product flavor (default: gms; also: huawei) ANDROID_BUILD_TYPE Native Android build type (default: debug; also: release) + IOS_DIR Native iOS SDK repo root (default: ../../OneSignal-iOS-SDK) + IOS_NATIVE_PROJECT Xcode project filename under examples/demo for the native + iOS demo (default: OneSignalSwiftUIExample.xcodeproj) + IOS_NATIVE_SCHEME Xcode scheme to build for the native iOS demo + (default: OneSignalSwiftUIExample) OS_VERSION Platform version (default: 26.2 / 16) IOS_SIMULATOR iOS simulator name (default: iPhone 17) IOS_RUNTIME simctl runtime id (default: iOS-26-2) @@ -160,15 +167,18 @@ prompt_choice() { done } -# --sdk=android implies --platform=android (the native demo only targets -# Android), so resolve PLATFORM first to skip the platform prompt when the -# user only passed --sdk=android. +# Native --sdk= implies --platform= (the native demos only +# target their own OS), so resolve PLATFORM first to skip the platform prompt +# when the user only passed --sdk=android or --sdk=ios. if [[ "${SDK_TYPE:-}" == "android" && -z "${PLATFORM:-}" ]]; then PLATFORM="android" fi +if [[ "${SDK_TYPE:-}" == "ios" && -z "${PLATFORM:-}" ]]; then + PLATFORM="ios" +fi prompt_choice PLATFORM "Select platform:" ios android -prompt_choice SDK_TYPE "Select SDK type:" flutter react-native cordova capacitor dotnet expo unity android +prompt_choice SDK_TYPE "Select SDK type:" flutter react-native cordova capacitor dotnet expo unity android ios case "$PLATFORM" in ios|android) ;; @@ -176,8 +186,8 @@ case "$PLATFORM" in esac case "$SDK_TYPE" in - flutter|react-native|cordova|capacitor|dotnet|expo|unity|android) ;; - *) error "SDK_TYPE must be 'flutter', 'react-native', 'cordova', 'capacitor', 'dotnet', 'expo', 'unity', or 'android', got '$SDK_TYPE'" ;; + flutter|react-native|cordova|capacitor|dotnet|expo|unity|android|ios) ;; + *) error "SDK_TYPE must be 'flutter', 'react-native', 'cordova', 'capacitor', 'dotnet', 'expo', 'unity', 'android', or 'ios', got '$SDK_TYPE'" ;; esac if [[ "$SDK_TYPE" == "android" && "$PLATFORM" != "android" ]]; then @@ -185,6 +195,11 @@ if [[ "$SDK_TYPE" == "android" && "$PLATFORM" != "android" ]]; then exit 0 fi +if [[ "$SDK_TYPE" == "ios" && "$PLATFORM" != "ios" ]]; then + warn "--sdk=ios only runs on --platform=ios; skipping --platform=$PLATFORM" + exit 0 +fi + # ── Real-device validation + signing setup ──────────────────────────────────── # When --device-real is set, we need a physical-device build and codesigning # inputs. Centralised here so the rest of the script stays simulator-shaped @@ -193,7 +208,7 @@ fi if [[ "$IOS_REAL_DEVICE" == true ]]; then [[ "$PLATFORM" == "ios" ]] || error "--device-real only supports --platform=ios" case "$SDK_TYPE" in - cordova|capacitor|react-native|expo) ;; + cordova|capacitor|react-native|expo|ios) ;; android) error "--device-real not applicable to --sdk=android (native Android)" ;; flutter|dotnet) error "--device-real not yet supported for $SDK_TYPE — patch run-local.sh's build_${SDK_TYPE//-/_}_ios to invoke the device build" ;; esac @@ -330,6 +345,13 @@ elif [[ "$SDK_TYPE" == "android" ]]; then esac # Gradle emits per-flavor/type APKs under app/build/outputs/apk///. APP_PATH="${APP_PATH:-$DEMO_DIR/app/build/outputs/apk/${ANDROID_FLAVOR}/${ANDROID_BUILD_TYPE}/app-${ANDROID_FLAVOR}-${ANDROID_BUILD_TYPE}.apk}" +elif [[ "$SDK_TYPE" == "ios" ]]; then + IOS_DIR="${IOS_DIR:-$SDK_ROOT/OneSignal-iOS-SDK}" + [[ -d "$IOS_DIR" ]] || error "Native iOS SDK not found at $IOS_DIR — set IOS_DIR in .env" + DEMO_DIR="$IOS_DIR/examples/demo" + IOS_NATIVE_PROJECT="${IOS_NATIVE_PROJECT:-OneSignalSwiftUIExample.xcodeproj}" + IOS_NATIVE_SCHEME="${IOS_NATIVE_SCHEME:-OneSignalSwiftUIExample}" + APP_PATH="${APP_PATH:-$DEMO_DIR/build/Build/Products/${IOS_BUILD_DIR}/${IOS_NATIVE_SCHEME}.app}" fi # ── Platform defaults ──────────────────────────────────────────────────────── @@ -1445,6 +1467,44 @@ build_android_native() { info "App built: $APP_PATH" } +build_ios_native() { + # Builds the native iOS demo directly so local SDK source changes (under + # OneSignal-iOS-SDK/) get exercised end-to-end. The demo wires the SDK in + # via Package.swift / podspec at the repo root; xcodebuild resolves those + # against the local checkout, mirroring how build_android_native uses the + # local OneSignalSDK module instead of a published artifact. + local proj_path="$DEMO_DIR/$IOS_NATIVE_PROJECT" + [[ -d "$proj_path" ]] || error "Xcode project not found at $proj_path — set IOS_NATIVE_PROJECT or IOS_DIR" + + if [[ -n "${ONESIGNAL_APP_ID:-}" ]]; then + info "Writing .env for demo app..." + cat > "$DEMO_DIR/.env" < dialog with empty "External User Id" field 3. LOGOUT USER button (only when logged in, outlined style) From 4b0d8681fa5aeaf5af3b93534cf27240a32b2ce6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Sun, 24 May 2026 16:20:59 -0700 Subject: [PATCH 02/11] refactor(appium): derive iOS scheme from project basename --- appium/scripts/run-local.sh | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index eef983d..c111c2a 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -110,9 +110,8 @@ Env vars (set in .env or export): ANDROID_BUILD_TYPE Native Android build type (default: debug; also: release) IOS_DIR Native iOS SDK repo root (default: ../../OneSignal-iOS-SDK) IOS_NATIVE_PROJECT Xcode project filename under examples/demo for the native - iOS demo (default: OneSignalSwiftUIExample.xcodeproj) - IOS_NATIVE_SCHEME Xcode scheme to build for the native iOS demo - (default: OneSignalSwiftUIExample) + iOS demo (default: App.xcodeproj). The scheme is derived + from the basename (XcodeGen convention). OS_VERSION Platform version (default: 26.2 / 16) IOS_SIMULATOR iOS simulator name (default: iPhone 17) IOS_RUNTIME simctl runtime id (default: iOS-26-2) @@ -349,9 +348,10 @@ elif [[ "$SDK_TYPE" == "ios" ]]; then IOS_DIR="${IOS_DIR:-$SDK_ROOT/OneSignal-iOS-SDK}" [[ -d "$IOS_DIR" ]] || error "Native iOS SDK not found at $IOS_DIR — set IOS_DIR in .env" DEMO_DIR="$IOS_DIR/examples/demo" - IOS_NATIVE_PROJECT="${IOS_NATIVE_PROJECT:-OneSignalSwiftUIExample.xcodeproj}" - IOS_NATIVE_SCHEME="${IOS_NATIVE_SCHEME:-OneSignalSwiftUIExample}" - APP_PATH="${APP_PATH:-$DEMO_DIR/build/Build/Products/${IOS_BUILD_DIR}/${IOS_NATIVE_SCHEME}.app}" + # XcodeGen names the scheme after the project, so we derive both the scheme + # and the .app artifact name from IOS_NATIVE_PROJECT's basename. + IOS_NATIVE_PROJECT="${IOS_NATIVE_PROJECT:-App.xcodeproj}" + APP_PATH="${APP_PATH:-$DEMO_DIR/build/Build/Products/${IOS_BUILD_DIR}/${IOS_NATIVE_PROJECT%.xcodeproj}.app}" fi # ── Platform defaults ──────────────────────────────────────────────────────── @@ -1469,12 +1469,23 @@ build_android_native() { build_ios_native() { # Builds the native iOS demo directly so local SDK source changes (under - # OneSignal-iOS-SDK/) get exercised end-to-end. The demo wires the SDK in - # via Package.swift / podspec at the repo root; xcodebuild resolves those - # against the local checkout, mirroring how build_android_native uses the - # local OneSignalSDK module instead of a published artifact. + # OneSignal-iOS-SDK/iOS_SDK/) get exercised end-to-end. The demo's + # App.xcodeproj has a projectReferences entry pointing at the SDK's own + # OneSignal.xcodeproj, so xcodebuild builds the local SDK frameworks + # transitively — mirroring how build_android_native uses the local + # OneSignalSDK module instead of a published artifact. + if [[ -f "$DEMO_DIR/project.yml" ]]; then + if command -v xcodegen >/dev/null 2>&1; then + info "Regenerating $IOS_NATIVE_PROJECT from project.yml (xcodegen)..." + (cd "$DEMO_DIR" && xcodegen generate --quiet) + else + warn "xcodegen not found; using existing $IOS_NATIVE_PROJECT (edits to project.yml will be ignored)" + fi + fi + local proj_path="$DEMO_DIR/$IOS_NATIVE_PROJECT" [[ -d "$proj_path" ]] || error "Xcode project not found at $proj_path — set IOS_NATIVE_PROJECT or IOS_DIR" + local scheme="${IOS_NATIVE_PROJECT%.xcodeproj}" if [[ -n "${ONESIGNAL_APP_ID:-}" ]]; then info "Writing .env for demo app..." @@ -1486,10 +1497,10 @@ EOF warn "ONESIGNAL_APP_ID not set — demo will fall back to its built-in default" fi - info "Building scheme '$IOS_NATIVE_SCHEME' (Release) for ${IOS_SDK}..." + info "Building scheme '$scheme' (Release) for ${IOS_SDK}..." (cd "$DEMO_DIR" && xcodebuild \ -project "$IOS_NATIVE_PROJECT" \ - -scheme "$IOS_NATIVE_SCHEME" \ + -scheme "$scheme" \ -configuration Release \ -sdk "$IOS_SDK" \ ${IOS_DESTINATION:+-destination} ${IOS_DESTINATION:+"$IOS_DESTINATION"} $IOS_XCODE_EXTRA_ARGS \ From 775ee2ca54d40c9969e59ae5f742a3f5a09a6f59 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Sun, 24 May 2026 16:26:48 -0700 Subject: [PATCH 03/11] refactor(appium): use Secrets.plist for iOS credentials --- appium/scripts/run-local.sh | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index c111c2a..f9ae469 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -1487,14 +1487,22 @@ build_ios_native() { [[ -d "$proj_path" ]] || error "Xcode project not found at $proj_path — set IOS_NATIVE_PROJECT or IOS_DIR" local scheme="${IOS_NATIVE_PROJECT%.xcodeproj}" - if [[ -n "${ONESIGNAL_APP_ID:-}" ]]; then - info "Writing .env for demo app..." - cat > "$DEMO_DIR/.env" < Date: Sun, 24 May 2026 16:30:06 -0700 Subject: [PATCH 04/11] perf(appium): cache iOS native build with hash --- appium/scripts/run-local.sh | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index f9ae469..682829f 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -1467,6 +1467,35 @@ build_android_native() { info "App built: $APP_PATH" } +# Hash every source/asset/config file that affects the compiled App.app for a +# native iOS demo build: demo sources (App/, the two extensions, project.yml, +# entitlements, the auto-written Secrets.plist), the regenerated .pbxproj, and +# the SDK framework source pulled in via projectReferences. Folds the SDK +# source into the demo hash so SDK edits cascade-invalidate the cached .app — +# same convention as dotnet_demo_inputs_hash / unity_demo_inputs_hash. Excludes +# test/mock targets (they only build under their own schemes, never "App") and +# xcodebuild-managed dirs. +ios_native_inputs_hash() { + find "$DEMO_DIR" "$IOS_DIR/iOS_SDK/OneSignalSDK" \ + -type f \ + ! -path "*/build/*" \ + ! -path "*/DerivedData/*" \ + ! -path "*/xcuserdata/*" \ + ! -path "*/.git/*" \ + ! -path "*Tests/*" \ + ! -path "*Mocks/*" \ + \( -name "*.swift" -o -name "*.h" -o -name "*.m" -o -name "*.mm" \ + -o -name "*.c" -o -name "*.plist" -o -name "*.entitlements" \ + -o -name "*.yml" -o -name "*.pbxproj" -o -name "*.modulemap" \ + -o -name "*.json" -o -name "*.wav" -o -name "*.png" \ + -o -name "*.xcprivacy" -o -name "*.storyboard" -o -name "*.strings" \) \ + 2>/dev/null \ + | sort \ + | xargs shasum 2>/dev/null \ + | shasum \ + | awk '{print $1}' +} + build_ios_native() { # Builds the native iOS demo directly so local SDK source changes (under # OneSignal-iOS-SDK/iOS_SDK/) get exercised end-to-end. The demo's @@ -1493,6 +1522,8 @@ build_ios_native() { # the App target. Overwrite unconditionally when either var is set so stale # CI values don't leak between runs; use `plutil` so API keys with XML- # special chars (&, <, ", etc.) round-trip safely without manual escaping. + # Done BEFORE the hash check so changing ONESIGNAL_APP_ID / ONESIGNAL_API_KEY + # automatically busts the cache (plutil's output is deterministic). local secrets="$DEMO_DIR/App/Secrets.plist" if [[ -n "${ONESIGNAL_APP_ID:-}" || -n "${ONESIGNAL_API_KEY:-}" ]]; then info "Writing Secrets.plist for demo app..." @@ -1505,6 +1536,19 @@ build_ios_native() { warn "ONESIGNAL_APP_ID / ONESIGNAL_API_KEY not set — demo will fall back to SecretsConfig.defaultAppId" fi + # Top-level skip: even an incremental xcodebuild costs ~30-60s on a no-op in + # resource copy, framework embed, codesign, and validation. Skip entirely + # when demo + SDK source + Secrets.plist + regenerated pbxproj all match a + # previous build. Mirrors build_expo_ios's stamp-based skip. + local build_stamp="$DEMO_DIR/build/.ios-native-build.stamp" + local build_hash + build_hash=$(ios_native_inputs_hash) + if [[ -d "$APP_PATH" ]] && [[ -f "$build_stamp" ]] && [[ "$(cat "$build_stamp")" == "$build_hash" ]]; then + info "Demo + SDK source unchanged, skipping iOS native rebuild" + info "App: $APP_PATH" + return + fi + info "Building scheme '$scheme' (Release) for ${IOS_SDK}..." (cd "$DEMO_DIR" && xcodebuild \ -project "$IOS_NATIVE_PROJECT" \ @@ -1521,6 +1565,8 @@ build_ios_native() { $IOS_SIGNING_ARGS) [[ -d "$APP_PATH" ]] || error ".app not found after build at $APP_PATH" + mkdir -p "$(dirname "$build_stamp")" + echo "$build_hash" > "$build_stamp" info "App built: $APP_PATH" } From 661b99463a1e9963830b3e718fa7b36bfdc3e561 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Sun, 24 May 2026 16:35:28 -0700 Subject: [PATCH 05/11] fix(appium): write Secrets.plist before xcodegen runs --- appium/scripts/run-local.sh | 64 ++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index 682829f..b617269 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -1503,39 +1503,57 @@ build_ios_native() { # OneSignal.xcodeproj, so xcodebuild builds the local SDK frameworks # transitively — mirroring how build_android_native uses the local # OneSignalSDK module instead of a published artifact. + + # The iOS demo reads credentials from a bundled Secrets.plist (the iOS + # equivalent of .env — see App/Services/SecretsConfig.swift). The file is + # gitignored and lives next to App/Info.plist; project.yml's explicit + # `buildPhase: resources` entry for App/Secrets.plist gets it copied into + # the App bundle. Use `plutil` so API keys with XML-special chars + # (&, <, ", etc.) round-trip safely without manual escaping. + # + # ALWAYS write the file (empty dict when env vars are unset) so xcodebuild's + # Copy Bundle Resources phase doesn't fail on a missing optional resource — + # SecretsConfig falls back to defaultAppId for any keys not present. + # + # Done BEFORE xcodegen (so the file reference is generated against a real + # on-disk file) and BEFORE the hash check (so changing ONESIGNAL_APP_ID / + # ONESIGNAL_API_KEY automatically busts the cache — plutil's output is + # deterministic). + local secrets="$DEMO_DIR/App/Secrets.plist" + if [[ -n "${ONESIGNAL_APP_ID:-}" || -n "${ONESIGNAL_API_KEY:-}" ]]; then + info "Writing Secrets.plist for demo app..." + else + warn "ONESIGNAL_APP_ID / ONESIGNAL_API_KEY not set — writing empty Secrets.plist; demo will fall back to SecretsConfig.defaultAppId" + fi + plutil -create xml1 "$secrets" + [[ -n "${ONESIGNAL_APP_ID:-}" ]] && \ + plutil -insert ONESIGNAL_APP_ID -string "$ONESIGNAL_APP_ID" "$secrets" + [[ -n "${ONESIGNAL_API_KEY:-}" ]] && \ + plutil -insert ONESIGNAL_API_KEY -string "$ONESIGNAL_API_KEY" "$secrets" + + # Only regenerate the .pbxproj when project.yml is newer than the existing + # generated file. xcodegen 2.45.x is NOT deterministic across no-op runs + # (each `xcodegen generate` produces a slightly different .pbxproj even + # with identical inputs), so unconditional regen leaves spurious unstaged + # changes in the iOS SDK repo on every script invocation. The mtime gate + # mirrors how other build steps (Podfile.lock stamp, cap sync stamp) skip + # work when their inputs are unchanged. + local proj_path="$DEMO_DIR/$IOS_NATIVE_PROJECT" + local pbxproj="$proj_path/project.pbxproj" if [[ -f "$DEMO_DIR/project.yml" ]]; then - if command -v xcodegen >/dev/null 2>&1; then + if ! command -v xcodegen >/dev/null 2>&1; then + warn "xcodegen not found; using existing $IOS_NATIVE_PROJECT (edits to project.yml will be ignored)" + elif [[ ! -f "$pbxproj" ]] || [[ "$DEMO_DIR/project.yml" -nt "$pbxproj" ]]; then info "Regenerating $IOS_NATIVE_PROJECT from project.yml (xcodegen)..." (cd "$DEMO_DIR" && xcodegen generate --quiet) else - warn "xcodegen not found; using existing $IOS_NATIVE_PROJECT (edits to project.yml will be ignored)" + info "$IOS_NATIVE_PROJECT up to date with project.yml, skipping xcodegen" fi fi - local proj_path="$DEMO_DIR/$IOS_NATIVE_PROJECT" [[ -d "$proj_path" ]] || error "Xcode project not found at $proj_path — set IOS_NATIVE_PROJECT or IOS_DIR" local scheme="${IOS_NATIVE_PROJECT%.xcodeproj}" - # The iOS demo reads credentials from a bundled Secrets.plist (the iOS - # equivalent of .env — see App/Services/SecretsConfig.swift). The file is - # gitignored and lives next to App/Info.plist so Xcode auto-bundles it with - # the App target. Overwrite unconditionally when either var is set so stale - # CI values don't leak between runs; use `plutil` so API keys with XML- - # special chars (&, <, ", etc.) round-trip safely without manual escaping. - # Done BEFORE the hash check so changing ONESIGNAL_APP_ID / ONESIGNAL_API_KEY - # automatically busts the cache (plutil's output is deterministic). - local secrets="$DEMO_DIR/App/Secrets.plist" - if [[ -n "${ONESIGNAL_APP_ID:-}" || -n "${ONESIGNAL_API_KEY:-}" ]]; then - info "Writing Secrets.plist for demo app..." - plutil -create xml1 "$secrets" - [[ -n "${ONESIGNAL_APP_ID:-}" ]] && \ - plutil -insert ONESIGNAL_APP_ID -string "$ONESIGNAL_APP_ID" "$secrets" - [[ -n "${ONESIGNAL_API_KEY:-}" ]] && \ - plutil -insert ONESIGNAL_API_KEY -string "$ONESIGNAL_API_KEY" "$secrets" - else - warn "ONESIGNAL_APP_ID / ONESIGNAL_API_KEY not set — demo will fall back to SecretsConfig.defaultAppId" - fi - # Top-level skip: even an incremental xcodebuild costs ~30-60s on a no-op in # resource copy, framework embed, codesign, and validation. Skip entirely # when demo + SDK source + Secrets.plist + regenerated pbxproj all match a From efa114f07231db02eda9a70f6900ad9b4d2a3146 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 25 May 2026 10:53:47 -0700 Subject: [PATCH 06/11] refactor(appium): consolidate iam tests into one --- appium/tests/specs/03_iam.spec.ts | 8 ++++---- demo/build.md | 13 ++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/appium/tests/specs/03_iam.spec.ts b/appium/tests/specs/03_iam.spec.ts index e68fc3b..b936d2b 100644 --- a/appium/tests/specs/03_iam.spec.ts +++ b/appium/tests/specs/03_iam.spec.ts @@ -29,11 +29,11 @@ describe('In-App Messaging', () => { { buttonId: 'send_iam_full_screen_button', expectedTitle: 'Full Screen' }, ]; - for (const iam of iamTypes) { - it(`can show ${iam.expectedTitle}`, async () => { + it('can show iam messages', async () => { + for (const iam of iamTypes) { await checkInAppMessage(iam); - }); - } + } + }); it('can pause iam', async () => { await scrollToEl('pause_iam_toggle'); diff --git a/demo/build.md b/demo/build.md index 7ae03d8..dc4ed47 100644 --- a/demo/build.md +++ b/demo/build.md @@ -209,11 +209,11 @@ Separate SectionCard titled "User": - Title: "Send In-App Message" with info icon - Four FULL-WIDTH buttons (not a grid): - 1. TOP BANNER - vertical-align-top icon, trigger: "iam_type" = "top_banner" - 2. BOTTOM BANNER - vertical-align-bottom icon, trigger: "iam_type" = "bottom_banner" - 3. CENTER MODAL - crop-square icon, trigger: "iam_type" = "center_modal" - 4. FULL SCREEN - fullscreen icon, trigger: "iam_type" = "full_screen" -- Styling: primary (red) background, white text, icon on LEFT, full width, left-aligned, UPPERCASE + 1. TOP BANNER - trigger: "iam_type" = "top_banner" + 2. BOTTOM BANNER - trigger: "iam_type" = "bottom_banner" + 3. CENTER MODAL - trigger: "iam_type" = "center_modal" + 4. FULL SCREEN - trigger: "iam_type" = "full_screen" +- Styling: primary (red) background, white text, centered, no icons, full width, UPPERCASE (same as other primary buttons) - On tap: upserts `iam_type` in Triggers list. No snackbar (silent action — see Prompt 7.6) ### Prompt 2.6 - Aliases Section @@ -591,12 +591,11 @@ Feedback messages are shown directly from the UI layer (not centralized in the s Only the following actions show snackbar feedback from the UI: -- Login/Logout: "Logged in as {userId}" / "User logged out" - Outcomes: "Outcome sent: {name}" / "Unique outcome sent: {name}" / "Outcome sent: {name} = {value}" - Custom Events: "Event tracked: {name}" - Location check: "Location shared: {bool}" -All other actions (add/remove items, notifications, IAM, live activities, etc.) use the platform's standard logging primitive only -- no snackbar. The state management layer should NOT hold snackbar state or expose snackbar messages. +All other actions (login/logout, add/remove items, notifications, IAM, live activities, etc.) use the platform's standard logging primitive only -- no snackbar. The state management layer should NOT hold snackbar state or expose snackbar messages. Logging: From 9cd69bcb3ca3602e3073b062d1147c70cbf25e53 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 25 May 2026 11:48:58 -0700 Subject: [PATCH 07/11] docs(demo): clarify empty state copy and UX rules --- demo/build.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/demo/build.md b/demo/build.md index dc4ed47..585969e 100644 --- a/demo/build.md +++ b/demo/build.md @@ -230,7 +230,7 @@ Separate SectionCard titled "User": - Title: "Emails" with info icon - List with X icon per item (remove action) -- "No Emails Added" when empty +- "No emails added" when empty - ADD EMAIL -> dialog with empty email field - Collapse when >5 items: show first 5, "X more" tappable to expand @@ -238,7 +238,7 @@ Separate SectionCard titled "User": - Title: "SMS" with info icon - Same pattern as Emails but for phone numbers -- "No SMS Added" when empty +- "No SMS added" when empty - ADD SMS -> dialog with empty SMS field ### Prompt 2.9 - Tags Section @@ -261,8 +261,9 @@ Separate SectionCard titled "User": ### Prompt 2.11 - Triggers Section (IN MEMORY ONLY) - Title: "Triggers" with info icon -- Same list/button pattern as Tags (ADD TRIGGER, ADD MULTIPLE TRIGGERS, REMOVE TRIGGERS), plus: +- Same list/button pattern as Tags (ADD TRIGGER, ADD MULTIPLE TRIGGERS, REMOVE TRIGGERS — hidden when the list is empty), plus: - CLEAR ALL TRIGGERS button (only when triggers exist) +- "No triggers added" when empty - Triggers are IN MEMORY ONLY: not persisted, cleared on restart - Sending an IAM also upserts `iam_type` in this list - Transient test data for IAM testing @@ -556,7 +557,7 @@ Single state container at app root. Holds all UI state with public getters. Expo - **SectionCard**: card with title, optional info icon, content slot, onInfoTap callback, optional `sectionKey` for accessibility identifiers (generates `{sectionKey}_section` on the container and `{sectionKey}_info_icon` on the info button) - **ToggleRow**: label, optional description, toggle control, optional `semanticsLabel` for accessibility identifier - **ActionButton**: PrimaryButton (filled) and DestructiveButton (outlined, for secondary/destructive actions), full-width, per styles.md. Both accept optional `semanticsLabel` for accessibility identifier. -- **ListWidgets**: PairItem (key-value + optional delete), SingleItem (value + delete), EmptyState, LoadingState (inline spinner shown in the empty-state slot while a fetch is in flight, per styles.md), CollapsibleList (5 items then expandable; accepts an optional `loading` flag that swaps EmptyState for LoadingState when items is empty), PairList. All list widgets accept a required `sectionKey` for generating accessibility identifiers (e.g. `{sectionKey}_pair_key_{keyText}`, `{sectionKey}_remove_{keyText}`, `{sectionKey}_loading`). +- **ListWidgets**: PairItem (key-value + optional delete), SingleItem (value + delete), EmptyState, LoadingState (inline spinner shown in the empty-state slot while a fetch is in flight, per styles.md), CollapsibleList (5 items then expandable; accepts an optional `loading` flag that swaps EmptyState for LoadingState when items is empty), PairList. All list widgets accept a required `sectionKey` for generating accessibility identifiers (e.g. `{sectionKey}_pair_key_{keyText}`, `{sectionKey}_remove_{keyText}`, `{sectionKey}_loading`). Empty list copy uses `"No added"` with lowercase item names and lowercase `added` (exception: `"No SMS added"` keeps `SMS` uppercase). - **Dialogs**: all full-width with consistent padding. Dialogs accept optional semantics label parameters for key inputs and confirm buttons (e.g. `keySemanticsLabel`, `valueSemanticsLabel`, `confirmSemanticsLabel`). - SingleInputDialog, PairInputDialog (same row), MultiPairInputDialog (dynamic rows, dividers, X to delete, batch submit), MultiSelectRemoveDialog (checkboxes, batch remove) - LoginDialog, OutcomeDialog, TrackEventDialog, CustomNotificationDialog, TooltipDialog @@ -575,6 +576,7 @@ Shared by Aliases, Tags, and Triggers ADD MULTIPLE buttons. Shared by Tags and Triggers REMOVE buttons. +- The section button that opens this dialog (`remove_tags_button`, `remove_triggers_button`) is **hidden entirely** when the list is empty. Do not show it disabled. - Checkbox per item, label shows key only - "Remove (N)" button shows selected count, disabled when none - Returns selected keys list From 36b8aa503e6e1e30b59eff0136a8546d812dc649 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 25 May 2026 15:21:59 -0700 Subject: [PATCH 08/11] fix(appium): open modal to radio button element --- appium/tests/specs/08_outcome.spec.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/appium/tests/specs/08_outcome.spec.ts b/appium/tests/specs/08_outcome.spec.ts index f9d1cd7..99e5b79 100644 --- a/appium/tests/specs/08_outcome.spec.ts +++ b/appium/tests/specs/08_outcome.spec.ts @@ -18,11 +18,11 @@ describe('Outcomes', () => { }); it('can send a normal outcome', async () => { - const nameInput = await openModal('send_outcome_button', 'outcome_name_input'); - await nameInput.setValue('test_normal'); - - const normalRadio = await byTestId('outcome_type_normal_radio'); + const normalRadio = await openModal('send_outcome_button', 'outcome_type_normal_radio'); await normalRadio.click(); + + const nameInput = await byTestId('outcome_name_input'); + await nameInput.setValue('test_normal'); const sendBtn = await byTestId('outcome_send_button'); await sendBtn.click(); @@ -31,11 +31,11 @@ describe('Outcomes', () => { }); it('can send a unique outcome', async () => { - const nameInput = await openModal('send_outcome_button', 'outcome_name_input'); - await nameInput.setValue('test_unique'); - - const uniqueRadio = await byTestId('outcome_type_unique_radio'); + const uniqueRadio = await openModal('send_outcome_button', 'outcome_type_unique_radio'); await uniqueRadio.click(); + + const nameInput = await byTestId('outcome_name_input'); + await nameInput.setValue('test_unique'); const sendBtn = await byTestId('outcome_send_button'); await sendBtn.click(); @@ -44,11 +44,10 @@ describe('Outcomes', () => { }); it('can send an outcome with value', async () => { - const nameInput = await openModal('send_outcome_button', 'outcome_name_input'); - - const withValueRadio = await byTestId('outcome_type_value_radio'); + const withValueRadio = await openModal('send_outcome_button', 'outcome_type_value_radio'); await withValueRadio.click(); + const nameInput = await byTestId('outcome_name_input'); await nameInput.setValue('test_valued'); const valueInput = await byTestId('outcome_value_input'); From 7e0fcb962f91eef354aaa38fb9ab8872c476995d Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 25 May 2026 16:06:55 -0700 Subject: [PATCH 09/11] fix(appium): simplify label for ios/android sdks --- appium/scripts/run-all.sh | 11 +++++++++-- appium/scripts/run-local.sh | 9 +++++++-- demo/build.md | 29 +++++++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/appium/scripts/run-all.sh b/appium/scripts/run-all.sh index e1da0bc..72601d0 100755 --- a/appium/scripts/run-all.sh +++ b/appium/scripts/run-all.sh @@ -97,6 +97,7 @@ declare -a RESULTS FAILED=0 BAILED=0 SKIPPED=0 +BAIL_OUT=0 for platform in "${PLATFORMS[@]}"; do for sdk in "${SDKS[@]}"; do @@ -117,7 +118,11 @@ for platform in "${PLATFORMS[@]}"; do fi continue fi - label="${sdk} / ${platform}" + if [[ "$sdk" == "ios" || "$sdk" == "android" ]]; then + label="${sdk}" + else + label="${sdk} / ${platform}" + fi echo "" echo -e "${BOLD}━━━ Running: ${label} ━━━${NC}" # `${arr[@]+"${arr[@]}"}` expands the array only when it has elements; @@ -129,11 +134,13 @@ for platform in "${PLATFORMS[@]}"; do FAILED=$((FAILED + 1)) if (( BAIL )); then BAILED=1 + BAIL_OUT=1 warn "Bailing out after first failure (--bail)" - break 2 + break fi fi done + (( BAIL_OUT )) && break done echo "" diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index b617269..b07e175 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -1557,8 +1557,13 @@ build_ios_native() { # Top-level skip: even an incremental xcodebuild costs ~30-60s on a no-op in # resource copy, framework embed, codesign, and validation. Skip entirely # when demo + SDK source + Secrets.plist + regenerated pbxproj all match a - # previous build. Mirrors build_expo_ios's stamp-based skip. - local build_stamp="$DEMO_DIR/build/.ios-native-build.stamp" + # previous build. Mirrors build_expo_ios's stamp-based skip. Stamp is + # scoped by IOS_BUILD_DIR so sim and device builds don't share cache state + # (matches build_dotnet_ios / build_unity_ios; without this, a sim→edit + # SDK→device→sim sequence overwrites the stamp with the post-edit hash + # while the pre-edit sim .app is still on disk, and the skip would serve + # the stale binary). + local build_stamp="$DEMO_DIR/build/.ios-native-build-${IOS_BUILD_DIR}.stamp" local build_hash build_hash=$(ios_native_inputs_hash) if [[ -d "$APP_PATH" ]] && [[ -f "$build_stamp" ]] && [[ "$(cat "$build_stamp")" == "$build_hash" ]]; then diff --git a/demo/build.md b/demo/build.md index 585969e..3e2166e 100644 --- a/demo/build.md +++ b/demo/build.md @@ -589,7 +589,11 @@ Implement theme constants/tokens mapping style reference to the platform's themi ### Prompt 7.6 - Feedback Messages (SnackBar/Toast) -Feedback messages are shown directly from the UI layer (not centralized in the state management layer). Use a `BuildContext` extension or helper that calls the platform's transient message API (SnackBar/Toast). The extension should hide the current message before showing a new one. Show snackbars from UI widget callbacks after awaiting the action, using a context-mounted check before displaying. +Feedback messages are shown directly from the UI layer (not centralized in the state management layer). Wrap the platform's transient message API (SnackBar/Toast) in a single UI-layer helper -- a `BuildContext` extension, a hook returned by a provider, an injected controller, a static helper, etc. -- and call it from widget/section callbacks after the SDK action runs. See each SDK's `examples/build.md` for the concrete helper name, file location, and wiring. + +**Replace on show:** when a new snackbar/toast is triggered while one is already visible, dismiss the current message immediately and show the new one (do not queue). Reset the 3s timer from when the new message appears. The helper must perform the dismiss-then-show internally so callers can fire `showSnackbar(...)` repeatedly without coordinating timers themselves. + +**Duration:** show every snackbar/toast for **3000ms (3 seconds)**. Use an explicit named constant (e.g. `TOAST_DURATION_MS = 3000`) rather than platform defaults, so all demos behave the same in manual use and Appium runs. Only the following actions show snackbar feedback from the UI: @@ -597,12 +601,33 @@ Only the following actions show snackbar feedback from the UI: - Custom Events: "Event tracked: {name}" - Location check: "Location shared: {bool}" -All other actions (login/logout, add/remove items, notifications, IAM, live activities, etc.) use the platform's standard logging primitive only -- no snackbar. The state management layer should NOT hold snackbar state or expose snackbar messages. +All other actions (login/logout, add/remove items, notifications, IAM, live activities, etc.) use the platform's standard logging primitive only -- no snackbar. The state management layer (ViewModel / Context / Store / etc.) MUST NOT hold snackbar state, expose snackbar messages (e.g. via `@Published`, `Flow`, `Channel`, observable events), or call the snackbar helper directly. Logging: - Use the platform's built-in logging primitive directly (`console.log`/`console.error` for JS/TS, `debugPrint` for Dart, `System.Diagnostics.Debug.WriteLine` for C#, `print`/`NSLog` for Swift, `Log.d`/`Log.e` for Kotlin/Java). +### Prompt 7.7 - Dialog Placement + +Each section owns the dialogs for its actions (login, add/remove, outcomes, track event, custom push). The main screen/page owns tooltip dialogs only. + +**Main screen/page pattern:** + +- Layout + the tooltip dialog only. Hold a single piece of tooltip state (active key or open boolean) on the main screen. +- Pass `onInfoTap` / `onInfoClick` callbacks keyed by section so the main screen can show the matching tooltip when a section's info icon is tapped. +- Do not centralize action dialog visibility on the main screen. +- Do not store action dialog visibility, dialog input drafts, or "is dialog open" flags on the ViewModel / state container. + +**Section pattern:** + +1. Section declares local UI state for each of its action dialogs (`*Open` booleans, `@State` properties, `remember { mutableStateOf(false) }`, code-behind handlers, or an imperative `showDialog(...)` call -- whichever is idiomatic). +2. Button handler triggers the matching dialog. +3. Dialog confirm handler calls the SDK action via the ViewModel / repository, closes the dialog, and emits a snackbar (per Prompt 7.6) when the action is in the allowed-snackbar list. + +**Shared dialog primitives** (single-input dialog, pair input, multi-pair, multi-select remove, login, outcome, track event, custom notification, tooltip) live in a platform-appropriate components folder; sections import and compose them locally rather than redeclaring per-section dialog markup. + +See each SDK's `examples/build.md` for the platform-specific helper APIs, the exact shared-dialog folder, and any platform-idiomatic wiring (UI-tree injection mechanism, dialog presentation API, code-behind vs. declarative state, etc.). + --- ## Configuration From f4afb769023d4dd665f29035af41b8653b65090b Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 26 May 2026 12:14:47 -0700 Subject: [PATCH 10/11] fix(appium): gate xcodegen on content hash not mtime --- appium/scripts/run-local.sh | 56 +++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index b07e175..a59fc04 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -1496,6 +1496,29 @@ ios_native_inputs_hash() { | awk '{print $1}' } +# Hash the inputs that affect xcodegen's pbxproj output: project.yml content +# plus the sorted file listing of the source-globbed target dirs. File +# listings (not contents) because pbxproj references files by path — only +# adds/removes/renames change it. Reads target source dirs out of project.yml +# itself rather than hardcoding (matches whatever xcodegen actually sees). +ios_pbxproj_inputs_hash() { + local yml="$DEMO_DIR/project.yml" + [[ -f "$yml" ]] || return 0 + { + shasum "$yml" 2>/dev/null + # Extract `- path: ` entries under `sources:` blocks. Anything + # exotic (per-file sources, conditional paths) falls back gracefully: + # if the resolved path isn't a directory, find just emits nothing. + awk '/^[[:space:]]*sources:/{in_src=1; next} + in_src && /^[[:space:]]*- path:/{print $3; next} + in_src && /^[^[:space:]-]/{in_src=0}' "$yml" \ + | while read -r src; do + [[ -d "$DEMO_DIR/$src" ]] && find "$DEMO_DIR/$src" -type f + done \ + | sort + } | shasum | awk '{print $1}' +} + build_ios_native() { # Builds the native iOS demo directly so local SDK source changes (under # OneSignal-iOS-SDK/iOS_SDK/) get exercised end-to-end. The demo's @@ -1531,23 +1554,34 @@ build_ios_native() { [[ -n "${ONESIGNAL_API_KEY:-}" ]] && \ plutil -insert ONESIGNAL_API_KEY -string "$ONESIGNAL_API_KEY" "$secrets" - # Only regenerate the .pbxproj when project.yml is newer than the existing - # generated file. xcodegen 2.45.x is NOT deterministic across no-op runs - # (each `xcodegen generate` produces a slightly different .pbxproj even - # with identical inputs), so unconditional regen leaves spurious unstaged - # changes in the iOS SDK repo on every script invocation. The mtime gate - # mirrors how other build steps (Podfile.lock stamp, cap sync stamp) skip - # work when their inputs are unchanged. + # Only regenerate the .pbxproj when its inputs change. xcodegen 2.45.x is + # NOT deterministic across no-op runs (each `xcodegen generate` produces a + # slightly different .pbxproj even with identical inputs), so unconditional + # regen leaves spurious unstaged changes in the iOS SDK repo on every + # script invocation. Gate on a hash of (project.yml content + sorted file + # listing of the source-globbed dirs) rather than mtime — mtime misses new + # files added to glob-sourced dirs (`App/Foo.swift` without touching + # project.yml leaves pbxproj newer than yml, gate skips, new file is + # missing from the build). File listings rather than contents because + # pbxproj references files by path; only adds/removes/renames affect it. local proj_path="$DEMO_DIR/$IOS_NATIVE_PROJECT" local pbxproj="$proj_path/project.pbxproj" + local pbxproj_stamp="$DEMO_DIR/build/.ios-native-pbxproj.stamp" if [[ -f "$DEMO_DIR/project.yml" ]]; then if ! command -v xcodegen >/dev/null 2>&1; then warn "xcodegen not found; using existing $IOS_NATIVE_PROJECT (edits to project.yml will be ignored)" - elif [[ ! -f "$pbxproj" ]] || [[ "$DEMO_DIR/project.yml" -nt "$pbxproj" ]]; then - info "Regenerating $IOS_NATIVE_PROJECT from project.yml (xcodegen)..." - (cd "$DEMO_DIR" && xcodegen generate --quiet) else - info "$IOS_NATIVE_PROJECT up to date with project.yml, skipping xcodegen" + local pbxproj_hash + pbxproj_hash=$(ios_pbxproj_inputs_hash) + if [[ ! -f "$pbxproj" ]] || [[ ! -f "$pbxproj_stamp" ]] \ + || [[ "$(cat "$pbxproj_stamp")" != "$pbxproj_hash" ]]; then + info "Regenerating $IOS_NATIVE_PROJECT from project.yml (xcodegen)..." + (cd "$DEMO_DIR" && xcodegen generate --quiet) + mkdir -p "$(dirname "$pbxproj_stamp")" + echo "$pbxproj_hash" > "$pbxproj_stamp" + else + info "$IOS_NATIVE_PROJECT up to date with project.yml + sources, skipping xcodegen" + fi fi fi From 8b323168386e6d685e6fad16efa5383b1e2ba3d0 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 26 May 2026 13:27:04 -0700 Subject: [PATCH 11/11] fix(appium): harden xcodegen cache and unify run-all skip labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ios_pbxproj_inputs_hash's project.yml awk parser with a wholesale demo-dir file-listing scan. The awk only matched the `- path:` long form and would silently drop dirs declared with any of XcodeGen's three other sources: forms — current project.yml is fine but a future-edit footgun. Hoist the run-all.sh label decision above the platform skip arms so SKIP, PASS, and FAIL rows share one schema instead of mixing short and long labels in the same summary table. Co-authored-by: Cursor --- appium/scripts/run-all.sh | 18 ++++++++++-------- appium/scripts/run-local.sh | 31 ++++++++++++++++++------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/appium/scripts/run-all.sh b/appium/scripts/run-all.sh index 72601d0..6a60d20 100755 --- a/appium/scripts/run-all.sh +++ b/appium/scripts/run-all.sh @@ -101,11 +101,18 @@ BAIL_OUT=0 for platform in "${PLATFORMS[@]}"; do for sdk in "${SDKS[@]}"; do - # Native demos only target their own platform. + # Native demos only target their own platform, so the platform suffix + # is redundant — keep the summary table consistent by using the short + # label in all branches (SKIP, PASS, FAIL). + if [[ "$sdk" == "ios" || "$sdk" == "android" ]]; then + label="${sdk}" + else + label="${sdk} / ${platform}" + fi if [[ "$sdk" == "android" && "$platform" == "ios" ]]; then if [[ -n "$PLATFORM_FILTER" ]]; then warn "--sdk=android only runs on --platform=android; skipping --platform=ios" - RESULTS+=("SKIP ${sdk} / ${platform}") + RESULTS+=("SKIP ${label}") SKIPPED=$((SKIPPED + 1)) fi continue @@ -113,16 +120,11 @@ for platform in "${PLATFORMS[@]}"; do if [[ "$sdk" == "ios" && "$platform" == "android" ]]; then if [[ -n "$PLATFORM_FILTER" ]]; then warn "--sdk=ios only runs on --platform=ios; skipping --platform=android" - RESULTS+=("SKIP ${sdk} / ${platform}") + RESULTS+=("SKIP ${label}") SKIPPED=$((SKIPPED + 1)) fi continue fi - if [[ "$sdk" == "ios" || "$sdk" == "android" ]]; then - label="${sdk}" - else - label="${sdk} / ${platform}" - fi echo "" echo -e "${BOLD}━━━ Running: ${label} ━━━${NC}" # `${arr[@]+"${arr[@]}"}` expands the array only when it has elements; diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index a59fc04..da93be2 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -1497,24 +1497,29 @@ ios_native_inputs_hash() { } # Hash the inputs that affect xcodegen's pbxproj output: project.yml content -# plus the sorted file listing of the source-globbed target dirs. File -# listings (not contents) because pbxproj references files by path — only -# adds/removes/renames change it. Reads target source dirs out of project.yml -# itself rather than hardcoding (matches whatever xcodegen actually sees). +# plus the sorted file listing of everything in the demo dir that xcodegen +# could plausibly glob. File listings (not contents) because pbxproj +# references files by path — only adds/removes/renames change it. We scan +# the whole demo dir rather than parsing project.yml's `sources:` entries +# because XcodeGen accepts four equivalent forms (shorthand, inline list, +# list of strings, list of dicts) — any path-extracting parser is a +# future-edit footgun. Over-scanning is harmless: a stray edit (e.g. to a +# README) just triggers one extra ~1s xcodegen run, no false skips. Excludes +# build artifacts and the generated .xcodeproj itself (regenerating it +# would self-bust the hash). ios_pbxproj_inputs_hash() { local yml="$DEMO_DIR/project.yml" [[ -f "$yml" ]] || return 0 { shasum "$yml" 2>/dev/null - # Extract `- path: ` entries under `sources:` blocks. Anything - # exotic (per-file sources, conditional paths) falls back gracefully: - # if the resolved path isn't a directory, find just emits nothing. - awk '/^[[:space:]]*sources:/{in_src=1; next} - in_src && /^[[:space:]]*- path:/{print $3; next} - in_src && /^[^[:space:]-]/{in_src=0}' "$yml" \ - | while read -r src; do - [[ -d "$DEMO_DIR/$src" ]] && find "$DEMO_DIR/$src" -type f - done \ + find "$DEMO_DIR" \ + -type f \ + ! -path "*/build/*" \ + ! -path "*/DerivedData/*" \ + ! -path "*/xcuserdata/*" \ + ! -path "*/.git/*" \ + ! -path "*/$IOS_NATIVE_PROJECT/*" \ + 2>/dev/null \ | sort } | shasum | awk '{print $1}' }