Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions appium/scripts/run-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ USAGE
done

PLATFORMS=(ios android)
SDKS=(cordova react-native flutter)
SDKS=(cordova react-native flutter dotnet)

declare -a RESULTS
FAILED=0
Expand All @@ -56,7 +56,9 @@ for platform in "${PLATFORMS[@]}"; do
label="${sdk} / ${platform}"
echo ""
echo -e "${BOLD}━━━ Running: ${label} ━━━${NC}"
if "$SCRIPT_DIR/run-local.sh" --platform="$platform" --sdk="$sdk" "${EXTRA_ARGS[@]}"; then
# `${arr[@]+"${arr[@]}"}` expands the array only when it has elements;
# under `set -u`, a bare `"${EXTRA_ARGS[@]}"` errors out on an empty array.
if "$SCRIPT_DIR/run-local.sh" --platform="$platform" --sdk="$sdk" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}; then
RESULTS+=("PASS ${label}")
else
RESULTS+=("FAIL ${label}")
Expand Down
265 changes: 244 additions & 21 deletions appium/scripts/run-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }

# ── Defaults ──────────────────────────────────────────────────────────────────
APPIUM_PORT="${APPIUM_PORT:-4723}"
SYSTEM_PORT="${SYSTEM_PORT:-}"
SKIP_BUILD=false
SKIP_DEVICE=false
SKIP_RESET=false
Expand All @@ -32,14 +33,16 @@ SPEC="tests/specs/**/*.spec.ts"
# ── Parse args ────────────────────────────────────────────────────────────────
for arg in "$@"; do
case "$arg" in
--platform=*) PLATFORM="${arg#--platform=}" ;;
--sdk=*) SDK_TYPE="${arg#--sdk=}" ;;
--device=*) DEVICE="${arg#--device=}" ;;
--skip) SKIP_BUILD=true; SKIP_DEVICE=true; SKIP_RESET=true ;;
--skip-build) SKIP_BUILD=true ;;
--skip-device) SKIP_DEVICE=true ;;
--skip-reset) SKIP_RESET=true ;;
--spec=*) SPEC="${arg#--spec=}" ;;
--platform=*) PLATFORM="${arg#--platform=}" ;;
--sdk=*) SDK_TYPE="${arg#--sdk=}" ;;
--device=*) DEVICE="${arg#--device=}" ;;
--appium-port=*) APPIUM_PORT="${arg#--appium-port=}" ;;
--system-port=*) SYSTEM_PORT="${arg#--system-port=}" ;;
--skip) SKIP_BUILD=true; SKIP_DEVICE=true; SKIP_RESET=true ;;
--skip-build) SKIP_BUILD=true ;;
--skip-device) SKIP_DEVICE=true ;;
--skip-reset) SKIP_RESET=true ;;
--spec=*) SPEC="${arg#--spec=}" ;;
--help|-h)
cat <<USAGE
Usage: $0 [OPTIONS]
Expand All @@ -51,15 +54,20 @@ PLATFORM and SDK are prompted interactively when not provided
via flags or env vars.

Options:
--platform=P ios | android
--sdk=S flutter | react-native | cordova
--device=NAME Device/simulator/AVD name (default: iPhone 17 / Samsung Galaxy S26)
--skip Skip build, device launch, and app reset (rerun tests only)
--skip-build Skip app build (reuse existing)
--skip-device Skip simulator/emulator launch
--skip-reset Keep existing app data
--spec=GLOB Spec glob (default: tests/specs/**/*.spec.ts)
-h, --help Show this help
--platform=P ios | android
--sdk=S flutter | react-native | cordova | dotnet
--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.
--system-port=N UiAutomator2 systemPort (Android only). Required when
running 2+ Android sessions in parallel; pick distinct
values per session (e.g. 8200, 8201).
--skip Skip build, device launch, and app reset (rerun tests only)
--skip-build Skip app build (reuse existing)
--skip-device Skip simulator/emulator launch
--skip-reset Keep existing app data
--spec=GLOB Spec glob (default: tests/specs/**/*.spec.ts)
-h, --help Show this help

Env vars (set in .env or export):
APP_PATH Path to .app/.apk (auto-detected if not set)
Expand All @@ -69,17 +77,24 @@ Env vars (set in .env or export):
FLUTTER_DIR Flutter SDK repo root (default: ../../OneSignal-Flutter-SDK)
RN_DIR React Native SDK repo root (default: ../../react-native-onesignal)
CORDOVA_DIR Cordova SDK repo root (default: ../../OneSignal-Cordova-SDK)
DOTNET_DIR .NET MAUI SDK repo root (default: ../../DotNet/OneSignal-DotNet-SDK)
DOTNET_TFM .NET target framework moniker base (default: net10.0)
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)
APPIUM_PORT Appium port (default: 4723)
APPIUM_PORT Appium port (default: 4723; same as --appium-port)
SYSTEM_PORT UiAutomator2 systemPort (same as --system-port)
USAGE
exit 0
;;
*) warn "Unknown option: $arg (ignored)" ;;
esac
done

# Ensure values set via CLI flags propagate to wdio (which reads them as env).
export APPIUM_PORT
[[ -n "$SYSTEM_PORT" ]] && export SYSTEM_PORT

# ── Prompt for required vars if not set ───────────────────────────────────────
prompt_choice() {
local var_name="$1" prompt_text="$2"
Expand Down Expand Up @@ -110,16 +125,16 @@ prompt_choice() {
}

prompt_choice PLATFORM "Select platform:" ios android
prompt_choice SDK_TYPE "Select SDK type:" flutter react-native cordova
prompt_choice SDK_TYPE "Select SDK type:" flutter react-native cordova dotnet

case "$PLATFORM" in
ios|android) ;;
*) error "PLATFORM must be 'ios' or 'android', got '$PLATFORM'" ;;
esac

case "$SDK_TYPE" in
flutter|react-native|cordova) ;;
*) error "SDK_TYPE must be 'flutter', 'react-native', or 'cordova', got '$SDK_TYPE'" ;;
flutter|react-native|cordova|dotnet) ;;
*) error "SDK_TYPE must be 'flutter', 'react-native', 'cordova', or 'dotnet', got '$SDK_TYPE'" ;;
esac

BUNDLE_ID="${BUNDLE_ID:-com.onesignal.example}"
Expand Down Expand Up @@ -151,6 +166,22 @@ elif [[ "$SDK_TYPE" == "cordova" ]]; then
else
APP_PATH="${APP_PATH:-$DEMO_DIR/android/app/build/outputs/apk/debug/app-debug.apk}"
fi
elif [[ "$SDK_TYPE" == "dotnet" ]]; then
DOTNET_DIR="${DOTNET_DIR:-$SDK_ROOT/DotNet/OneSignal-DotNet-SDK}"
[[ -d "$DOTNET_DIR" ]] || error ".NET MAUI SDK not found at $DOTNET_DIR — set DOTNET_DIR in .env"
DEMO_DIR="$DOTNET_DIR/examples/demo"
DOTNET_TFM="${DOTNET_TFM:-net10.0}"
if [[ "$PLATFORM" == "ios" ]]; then
# iOS simulator RID is arch-specific; auto-detect host arch (Apple Silicon vs Intel).
case "$(uname -m)" in
arm64) DOTNET_IOS_RID="${DOTNET_IOS_RID:-iossimulator-arm64}" ;;
x86_64) DOTNET_IOS_RID="${DOTNET_IOS_RID:-iossimulator-x64}" ;;
*) error "Unsupported host arch for .NET iOS sim build: $(uname -m)" ;;
esac
APP_PATH="${APP_PATH:-$DEMO_DIR/bin/Debug/${DOTNET_TFM}-ios/${DOTNET_IOS_RID}/demo.app}"
else
APP_PATH="${APP_PATH:-$DEMO_DIR/bin/Debug/${DOTNET_TFM}-android/com.onesignal.example-Signed.apk}"
fi
fi

# ── Platform defaults ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -464,6 +495,190 @@ build_cordova_android() {
info "App built: $APP_PATH"
}

write_dotnet_demo_env() {
if [[ -n "${ONESIGNAL_APP_ID:-}" && -n "${ONESIGNAL_API_KEY:-}" ]]; then
info "Writing .env for demo app..."
cat > "$DEMO_DIR/.env" <<EOF
ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID
ONESIGNAL_API_KEY=$ONESIGNAL_API_KEY
E2E_MODE=true
EOF
else
warn "ONESIGNAL_APP_ID / ONESIGNAL_API_KEY not set — skipping demo .env"
fi
}

# SDK + binding source roots for a given platform. The SDK rarely changes on
# day-to-day demo work, so we hash and build it independently from the demo and
# fold its hash into the demo input hash so SDK edits still cascade-invalidate.
dotnet_sdk_paths() {
local platform="$1" # ios | android
echo "$DOTNET_DIR/OneSignalSDK.DotNet"
echo "$DOTNET_DIR/OneSignalSDK.DotNet.Core"
echo "$DOTNET_DIR/Directory.Build.props"
if [[ "$platform" == "ios" ]]; then
echo "$DOTNET_DIR/OneSignalSDK.DotNet.iOS"
echo "$DOTNET_DIR/OneSignalSDK.DotNet.iOS.Binding"
else
echo "$DOTNET_DIR/OneSignalSDK.DotNet.Android"
echo "$DOTNET_DIR/OneSignalSDK.DotNet.Android.Core.Binding"
echo "$DOTNET_DIR/OneSignalSDK.DotNet.Android.InAppMessages.Binding"
echo "$DOTNET_DIR/OneSignalSDK.DotNet.Android.Location.Binding"
echo "$DOTNET_DIR/OneSignalSDK.DotNet.Android.Notifications.Binding"
fi
}

# Hash any source/metadata file under the given roots that can affect compiled
# output. Used by both the SDK and demo hashes; centralised so the inclusion
# rules stay in sync.
dotnet_hash_paths() {
find "$@" \
-type f \
! -path "*/bin/*" \
! -path "*/obj/*" \
! -path "*/artifacts/*" \
\( -name "*.cs" -o -name "*.csproj" -o -name "*.props" \
-o -name "*.targets" -o -name "*.xaml" -o -name "*.plist" \
-o -name "*.xml" -o -name "*.json" -o -name "*.png" \
-o -name "*.aar" -o -name "*.jar" -o -name ".env" \) \
2>/dev/null \
| sort \
| xargs shasum 2>/dev/null \
| shasum \
| awk '{print $1}'
}

dotnet_sdk_inputs_hash() {
local platform="$1"
local roots=()
while IFS= read -r p; do roots+=("$p"); done < <(dotnet_sdk_paths "$platform")
dotnet_hash_paths "${roots[@]}"
}

# Demo hash folds in the SDK hash so an SDK edit busts the demo cache too.
dotnet_demo_inputs_hash() {
local platform="$1"
local sdk_hash="$2"
local demo_hash
demo_hash=$(dotnet_hash_paths "$DEMO_DIR")
printf '%s\n%s\n' "$sdk_hash" "$demo_hash" | shasum | awk '{print $1}'
}

# Returns 0 if a previous build's stamp matches the current input hash AND the
# expected output artifact still exists, in which case the caller should skip.
dotnet_build_is_cached() {
local stamp="$1" artifact="$2" hash="$3"
[[ -e "$artifact" ]] || return 1
[[ -f "$stamp" ]] || return 1
[[ "$(cat "$stamp")" == "$hash" ]] || return 1
return 0
}

# Build only the SDK + binding projects for the given platform. The demo's
# csproj has a ProjectReference to `OneSignalSDK.DotNet`, so building that one
# project transitively builds every binding it pulls in for the target TFM.
# Cached separately so demo-only edits don't pay the SDK build cost.
build_dotnet_sdk() {
local platform="$1" # ios | android
local tfm="${DOTNET_TFM}-${platform}"
local sdk_proj="$DOTNET_DIR/OneSignalSDK.DotNet/OneSignalSDK.DotNet.csproj"
local sdk_dll="$DOTNET_DIR/OneSignalSDK.DotNet/bin/Debug/${tfm}/OneSignalSDK.DotNet.dll"
local stamp="$DOTNET_DIR/OneSignalSDK.DotNet/bin/Debug/.sdk-build-${platform}.stamp"
local hash="$2"

if dotnet_build_is_cached "$stamp" "$sdk_dll" "$hash"; then
info ".NET SDK unchanged, skipping SDK rebuild"
return
fi

info "Building .NET SDK + bindings for ${tfm}..."
dotnet build "$sdk_proj" -c Debug -f "$tfm"

[[ -f "$sdk_dll" ]] || error "SDK build did not produce $sdk_dll"
mkdir -p "$(dirname "$stamp")"
echo "$hash" > "$stamp"
}

build_dotnet_ios() {
write_dotnet_demo_env

command -v dotnet >/dev/null 2>&1 || error "dotnet CLI not found in PATH — install the .NET SDK"

local sdk_hash demo_hash
sdk_hash=$(dotnet_sdk_inputs_hash ios)
demo_hash=$(dotnet_demo_inputs_hash ios "$sdk_hash")

local stamp="$DEMO_DIR/bin/Debug/.dotnet-build-ios-${DOTNET_IOS_RID}.stamp"
if dotnet_build_is_cached "$stamp" "$APP_PATH" "$demo_hash"; then
info ".NET SDK + demo source unchanged, skipping rebuild"
info "App: $APP_PATH"
return
fi

build_dotnet_sdk ios "$sdk_hash"

# --no-dependencies: SDK is already built (and cached) by build_dotnet_sdk,
# so MSBuild can skip even checking referenced projects for up-to-date.
info "Building Debug .app for iOS simulator (${DOTNET_IOS_RID})..."
(cd "$DEMO_DIR" && dotnet build demo.csproj \
-c Debug \
-f "${DOTNET_TFM}-ios" \
-p:RuntimeIdentifier="${DOTNET_IOS_RID}" \
--no-dependencies)

[[ -d "$APP_PATH" ]] || error ".app not found after build at $APP_PATH"
mkdir -p "$(dirname "$stamp")"
echo "$demo_hash" > "$stamp"
info "App built: $APP_PATH"
}

build_dotnet_android() {
write_dotnet_demo_env

command -v dotnet >/dev/null 2>&1 || error "dotnet CLI not found in PATH — install the .NET SDK"

local sdk_hash demo_hash
sdk_hash=$(dotnet_sdk_inputs_hash android)
demo_hash=$(dotnet_demo_inputs_hash android "$sdk_hash")

local stamp="$DEMO_DIR/bin/Debug/.dotnet-build-android.stamp"
if dotnet_build_is_cached "$stamp" "$APP_PATH" "$demo_hash"; then
info ".NET SDK + demo source unchanged, skipping rebuild"
info "App: $APP_PATH"
return
fi

build_dotnet_sdk android "$sdk_hash"

# EmbedAssembliesIntoApk=true: by default `dotnet build -c Debug` for Android
# uses Fast Deployment, which leaves the managed assemblies *out* of the APK
# and pushes them live to /data/.../files/.__override__/<abi>/ via
# `-t:Run`. Appium just installs the APK, so without this flag monodroid
# aborts at startup with "No assemblies found in ... Fast Deployment. Exiting".
#
# AndroidLinkMode=None: skips the IL linker/trimmer pass on every demo edit.
# The linker normally trims unused IL across SDK + bindings + demo, which is
# ~15-25s of fixed cost per build. Debug builds don't need it (slightly larger
# APK is fine on the dev loop) and turning it off keeps incremental rebuilds
# of demo-only changes well under a minute.
#
# --no-dependencies: SDK already built above, so we skip MSBuild's
# up-to-date check on every referenced project.
info "Building Debug signed APK with embedded assemblies..."
(cd "$DEMO_DIR" && dotnet build demo.csproj \
-c Debug \
-f "${DOTNET_TFM}-android" \
-p:EmbedAssembliesIntoApk=true \
-p:AndroidUseFastDeployment=false \
-p:AndroidLinkMode=None \
--no-dependencies)

[[ -f "$APP_PATH" ]] || error ".apk not found after build at $APP_PATH"
mkdir -p "$(dirname "$stamp")"
echo "$demo_hash" > "$stamp"
info "App built: $APP_PATH"
}

build_app() {
if [[ "$SKIP_BUILD" == true ]]; then
if [[ "$PLATFORM" == "ios" && ! -d "$APP_PATH" ]] || [[ "$PLATFORM" == "android" && ! -f "$APP_PATH" ]]; then
Expand Down Expand Up @@ -491,6 +706,12 @@ build_app() {
else
build_cordova_android
fi
elif [[ "$SDK_TYPE" == "dotnet" ]]; then
if [[ "$PLATFORM" == "ios" ]]; then
build_dotnet_ios
else
build_dotnet_android
fi
fi
}

Expand Down Expand Up @@ -636,6 +857,8 @@ run_tests() {
BUNDLE_ID="${BUNDLE_ID:-}" \
ONESIGNAL_APP_ID="${ONESIGNAL_APP_ID:-}" \
ONESIGNAL_API_KEY="${ONESIGNAL_API_KEY:-}" \
APPIUM_PORT="$APPIUM_PORT" \
SYSTEM_PORT="${SYSTEM_PORT:-}" \
bunx wdio run "$conf" --spec "$SPEC"
}

Expand Down
Loading
Loading