diff --git a/appium/README.md b/appium/README.md index bf13e2c..8fb4429 100644 --- a/appium/README.md +++ b/appium/README.md @@ -44,12 +44,17 @@ cp .env.example .env # add your OneSignal credentials Tests are numbered to run in dependency order (user login happens before push/email/SMS tests that require a logged-in user): -| File | Description | -|---|---| -| `1_user.spec.ts` | Login, logout, anonymous state | -| `2_push.spec.ts` | Push subscription | -| `3_iam.spec.ts` | In-app messaging | -| `4_alias.spec.ts` | Alias operations | -| `5_email.spec.ts` | Email subscription | -| `6_sms.spec.ts` | SMS subscription | -| `7_tag.spec.ts` | Tag operations | +| File | Description | +| --------------------- | ------------------------------ | +| `01_user.spec.ts` | Login, logout, anonymous state | +| `02_push.spec.ts` | Push subscription | +| `03_iam.spec.ts` | In-app messaging | +| `04_alias.spec.ts` | Alias operations | +| `05_email.spec.ts` | Email subscription | +| `06_sms.spec.ts` | SMS subscription | +| `07_tag.spec.ts` | Tag operations | +| `08_outcome.spec.ts` | Outcome sending | +| `09_trigger.spec.ts` | Trigger add/remove/clear | +| `10_event.spec.ts` | Custom event tracking | +| `11_location.spec.ts` | Location prompting and sharing | +| `12_activity.spec.ts` | iOS Live Activity lifecycle | diff --git a/appium/scripts/.env.example b/appium/scripts/.env.example index c756524..3e288c8 100644 --- a/appium/scripts/.env.example +++ b/appium/scripts/.env.example @@ -8,14 +8,13 @@ ONESIGNAL_API_KEY=your-api-key # BUNDLE_ID=com.onesignal.example # ── Optional (defaults shown) ──────────────────────────────────────────────── -# DEVICE=iPhone 17 # iOS default; Android default: Google Pixel 8 -# OS_VERSION=26.2 # iOS default; Android default: 14 +# OS_VERSION=26.2 # iOS default; Android default: 16 # iOS-only -# IOS_SIMULATOR=iPhone 17 # Simulator name (defaults to DEVICE) +# IOS_SIMULATOR=iPhone 17 # Simulator name (defaults to --device) # IOS_RUNTIME=iOS-26-2 # simctl runtime identifier # Android-only -# AVD_NAME=Pixel_8 # AVD to boot +# AVD_NAME=Samsung_Galaxy_S26 # Defaults to --device with spaces replaced by underscores # APPIUM_PORT=4723 diff --git a/appium/scripts/README.md b/appium/scripts/README.md index 5947c50..cd27fc2 100644 --- a/appium/scripts/README.md +++ b/appium/scripts/README.md @@ -42,16 +42,16 @@ If `--platform` or `--sdk` are not provided, the script prompts interactively. ### Options -| Flag | Description | -|---|---| -| `--platform=P` | `ios` or `android` | -| `--sdk=S` | `flutter` or `react-native` | -| `--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 | -| `-h, --help` | Show help | +| Flag | Description | +| --------------- | ----------------------------------------------------------- | +| `--platform=P` | `ios` or `android` | +| `--sdk=S` | `flutter` or `react-native` | +| `--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 | +| `-h, --help` | Show help | ### Examples @@ -64,7 +64,13 @@ Run all tests (full build + fresh install): Run a single spec file: ```bash -./run-local.sh --platform=ios --sdk=flutter --spec="tests/specs/1_user.spec.ts" +./run-local.sh --platform=ios --sdk=flutter --spec="tests/specs/01_user.spec.ts" +``` + +Run multiple spec files: + +```bash +./run-local.sh --platform=ios --sdk=flutter --spec="tests/specs/{01_user,08_outcome}.spec.ts" ``` Re-run tests without rebuilding or relaunching the simulator: @@ -83,19 +89,19 @@ Skip only the build (simulator + reset still happen): All env vars can be set in `.env` or exported in your shell. See [`.env.example`](.env.example) for the full list. -| Variable | Default | Description | -|---|---|---| -| `ONESIGNAL_APP_ID` | -- | OneSignal app ID (written to demo app `.env`) | -| `ONESIGNAL_API_KEY` | -- | OneSignal REST API key | -| `FLUTTER_DIR` | `../../OneSignal-Flutter-SDK` | Path to the Flutter SDK repo | -| `APP_PATH` | auto-detected from build | Path to `.app` or `.apk` | -| `BUNDLE_ID` | `com.onesignal.example` | App bundle/package ID | -| `DEVICE` | `iPhone 17` / `Google Pixel 8` | Device name for WebdriverIO | -| `OS_VERSION` | `26.2` / `14` | Platform version | -| `IOS_SIMULATOR` | same as `DEVICE` | Simulator name for `simctl` | -| `IOS_RUNTIME` | `iOS-26-2` | simctl runtime identifier | -| `AVD_NAME` | `Pixel_8` | Android AVD name | -| `APPIUM_PORT` | `4723` | Appium server port | +| Variable | Default | Description | +| ------------------- | ---------------------------------- | --------------------------------------------- | +| `ONESIGNAL_APP_ID` | -- | OneSignal app ID (written to demo app `.env`) | +| `ONESIGNAL_API_KEY` | -- | OneSignal REST API key | +| `FLUTTER_DIR` | `../../OneSignal-Flutter-SDK` | Path to the Flutter SDK repo | +| `APP_PATH` | auto-detected from build | Path to `.app` or `.apk` | +| `BUNDLE_ID` | `com.onesignal.example` | App bundle/package ID | +| `DEVICE` | `iPhone 17` / `Samsung Galaxy S26` | Device name for WebdriverIO | +| `OS_VERSION` | `26.2` / `16` | Platform version | +| `IOS_SIMULATOR` | same as `DEVICE` | Simulator name for `simctl` | +| `IOS_RUNTIME` | `iOS-26-2` | simctl runtime identifier | +| `AVD_NAME` | `Samsung_Galaxy_S26` | Android AVD name | +| `APPIUM_PORT` | `4723` | Appium server port | ## Troubleshooting diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index 0f011a7..0ea9838 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -34,6 +34,7 @@ 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 ;; @@ -52,6 +53,7 @@ via flags or env vars. Options: --platform=P ios | android --sdk=S flutter | react-native + --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 @@ -64,9 +66,7 @@ Env vars (set in .env or export): BUNDLE_ID Bundle/package id (default: com.onesignal.example) ONESIGNAL_APP_ID OneSignal app ID (written to demo app .env) ONESIGNAL_API_KEY OneSignal REST API key (written to demo app .env) - DEVICE Device name for wdio (default: iPhone 17 / Google Pixel 8) - OS_VERSION Platform version (default: 26.2 / 14) - AVD_NAME Android AVD to boot (default: Pixel_8) + 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) @@ -114,7 +114,6 @@ case "$PLATFORM" in *) error "PLATFORM must be 'ios' or 'android', got '$PLATFORM'" ;; esac -[[ "$PLATFORM" == "android" ]] && error "Android is not supported yet. Only iOS is available for now." [[ "$SDK_TYPE" != "flutter" ]] && error "Only flutter is supported for now. Got '$SDK_TYPE'." BUNDLE_ID="${BUNDLE_ID:-com.onesignal.example}" @@ -123,7 +122,11 @@ if [[ "$SDK_TYPE" == "flutter" ]]; then FLUTTER_DIR="${FLUTTER_DIR:-$SDK_ROOT/OneSignal-Flutter-SDK}" [[ -d "$FLUTTER_DIR" ]] || error "Flutter SDK not found at $FLUTTER_DIR — set FLUTTER_DIR in .env" DEMO_DIR="$FLUTTER_DIR/examples/demo" - APP_PATH="${APP_PATH:-$DEMO_DIR/build/ios/iphonesimulator/Runner.app}" + if [[ "$PLATFORM" == "ios" ]]; then + APP_PATH="${APP_PATH:-$DEMO_DIR/build/ios/iphonesimulator/Runner.app}" + else + APP_PATH="${APP_PATH:-$DEMO_DIR/build/app/outputs/flutter-apk/app-debug.apk}" + fi fi # ── Platform defaults ──────────────────────────────────────────────────────── @@ -133,9 +136,9 @@ if [[ "$PLATFORM" == "ios" ]]; then IOS_SIMULATOR="${IOS_SIMULATOR:-$DEVICE}" IOS_RUNTIME="${IOS_RUNTIME:-iOS-26-2}" else - DEVICE="${DEVICE:-Google Pixel 8}" - OS_VERSION="${OS_VERSION:-14}" - AVD_NAME="${AVD_NAME:-Pixel_8}" + DEVICE="${DEVICE:-Samsung Galaxy S26}" + OS_VERSION="${OS_VERSION:-16}" + AVD_NAME="${AVD_NAME:-${DEVICE// /_}}" fi # ── 1. Build app ───────────────────────────────────────────────────────────── @@ -164,6 +167,28 @@ EOF info "App built: $APP_PATH" } +build_flutter_android() { + if [[ -n "${ONESIGNAL_APP_ID:-}" && -n "${ONESIGNAL_API_KEY:-}" ]]; then + info "Writing .env for demo app..." + cat > "$DEMO_DIR/.env" < { + try { + await driver.waitUntil( + async () => { + try { + const buttons = await driver.execute('mobile: alert', { action: 'getButtons' }); + return Array.isArray(buttons) && buttons.length > 0; + } catch { + return false; + } + }, + { timeout: timeoutMs, interval: 250 }, + ); + return await driver.getAlertText(); + } catch { + return null; + } +} + /** * Wait for the app to fully launch and the home screen to be visible. - * Uses the log view container as the sentinel element since it's present - * on the home screen of all demo apps. */ export async function waitForAppReady(opts: { skipLogin?: boolean } = {}) { const { skipLogin = false } = opts; - const logView = await byTestId('log_view_container'); - await logView.waitForDisplayed({ timeout: 5_000 }); + const mainScroll = await byTestId('main_scroll_view'); + await mainScroll.waitForDisplayed({ timeout: 5_000 }); - const testUserId = getTestExternalId(); + const alertHandled = await browser.sharedStore.get('alertHandled'); + if (!alertHandled) { + const alert = await waitForAlert(); + if (alert) await driver.acceptAlert(); + await browser.sharedStore.set('alertHandled', true); + } if (skipLogin) return; + // want to login user so we can't clean up/delete user data for the next rerun + const testUserId = getTestExternalId(); const loggedIn = await browser.sharedStore.get('loggedIn'); if (!loggedIn) { const userIdEl = await scrollToEl('user_external_id_value', { direction: 'up' }); @@ -156,14 +184,14 @@ export async function addTag(key: string, value: string) { const addButton = await byTestId('add_tag_button'); await addButton.click(); - const keyInput = await byTestId('multi_pair_key_0'); + const keyInput = await byTestId('tag_key_input'); await keyInput.waitForDisplayed({ timeout: 5_000 }); await keyInput.setValue(key); - const valueInput = await byTestId('multi_pair_value_0'); + const valueInput = await byTestId('tag_value_input'); await valueInput.setValue(value); - const confirmButton = await byTestId('multi_pair_confirm_button'); + const confirmButton = await byTestId('tag_confirm_button'); await confirmButton.click(); } @@ -185,11 +213,26 @@ export async function expectPairInSection(sectionId: string, key: string, value: } /** - * Clear the log view. + * Lock the iOS screen and wake it to reveal the lock screen (with notifications). */ -export async function clearLogs() { - const clearButton = await byTestId('log_view_clear_button'); - await clearButton.click(); +export async function lockScreen() { + await driver.updateSettings({ defaultActiveApplication: 'com.apple.springboard' }); + await driver.lock(); + await driver.pause(500); + + await driver.execute('mobile: pressButton', { name: 'home' }); + await driver.pause(500); +} + +/** + * Return to the app from SpringBoard / lock screen. + */ +export async function returnToApp() { + const caps = driver.capabilities as Record; + const bundleId = (caps['bundleId'] ?? caps['appium:bundleId']) as string; + await driver.updateSettings({ defaultActiveApplication: bundleId }); + await driver.execute('mobile: activateApp', { bundleId }); + await driver.pause(1_000); } /** @@ -212,9 +255,8 @@ export async function clearAllNotifications() { * Android: opens the notification shade, verifies the title (and optionally * body) are visible, then closes the shade. * - * iOS: goes to the home screen, switches to the SpringBoard context, then - * uses W3C touch actions (viewport origin) to swipe down and open the - * notification center. After verifying the notification, it returns to the app. + * iOS: swipes down from the top-left to open the notification center, + * verifies the notification, then returns to the app. */ export async function waitForNotification(opts: { title: string; @@ -274,19 +316,13 @@ export async function waitForNotification(opts: { return; } - // iOS: swipe down from the top-left of the screen to open notification center + // iOS: swipe down from the top-left to open notification center // (top-right opens Control Center on iOS 16+) - const caps = driver.capabilities as Record; - const bundleId = (caps['bundleId'] ?? caps['appium:bundleId']) as string; + await driver.updateSettings({ defaultActiveApplication: 'com.apple.springboard' }); await driver.execute('mobile: pressButton', { name: 'home' }); await driver.pause(1_000); - await driver.updateSettings({ - defaultActiveApplication: 'com.apple.springboard', - }); - await driver.pause(500); - const { width, height } = await driver.getWindowSize(); await driver.performActions([ { @@ -349,11 +385,7 @@ export async function waitForNotification(opts: { await image.waitForDisplayed({ timeout: 5_000 }); } - await driver.execute('mobile: pressButton', { name: 'home' }); - await driver.pause(500); - - await driver.updateSettings({ defaultActiveApplication: bundleId }); - await driver.execute('mobile: activateApp', { bundleId }); + await returnToApp(); } export async function checkNotification(opts: { diff --git a/appium/tests/helpers/logger.ts b/appium/tests/helpers/logger.ts index 4489554..cda300c 100644 --- a/appium/tests/helpers/logger.ts +++ b/appium/tests/helpers/logger.ts @@ -1,58 +1,38 @@ -import { byTestId } from './selectors.js'; +import { getPlatform } from './selectors.js'; -/** - * Get the current log entry count shown in the header badge. - */ -export async function getLogCount(): Promise { - const countEl = await byTestId('log_view_count'); - const text = await countEl.getText(); - const match = text.match(/\d+/); - return match ? parseInt(match[0], 10) : 0; -} - -/** - * Get the text of a specific log entry by index. - */ -export async function getLogMessage(index: number): Promise { - const messageEl = await byTestId(`log_entry_${index}_message`); - return messageEl.getText(); -} +const BUNDLE_ID = process.env.BUNDLE_ID || 'com.onesignal.example'; +const collectedLogs: string[] = []; -/** - * Get the level (info/warn/error) of a specific log entry. - */ -export async function getLogLevel(index: number): Promise { - const levelEl = await byTestId(`log_entry_${index}_level`); - return levelEl.getText(); +function drainLogs() { + const logType = getPlatform() === 'ios' ? 'syslog' : 'logcat'; + return driver.getLogs(logType); } -/** - * Check whether any log entry contains the given substring. - * Scans entries 0..count-1. - */ -export async function hasLogContaining(substring: string): Promise { - const count = await getLogCount(); - for (let i = 0; i < count; i++) { - const msg = await getLogMessage(i); - if (msg.includes(substring)) { - return true; +async function collectNewLogs(): Promise { + const entries = await drainLogs(); + for (const entry of entries) { + const msg = String((entry as Record).message ?? entry); + if (msg.includes(BUNDLE_ID)) { + collectedLogs.push(msg); } } - return false; } -/** - * Wait until a log entry containing the substring appears, - * polling at the given interval. - */ +export function hasLogContaining(substring: string): boolean { + return collectedLogs.some((msg) => msg.includes(substring)); +} + +// Avoid using this function and rely on snackbars instead export async function waitForLog( substring: string, timeoutMs = 30_000, pollMs = 1_000, ): Promise { + collectedLogs.length = 0; const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - if (await hasLogContaining(substring)) { + await collectNewLogs(); + if (hasLogContaining(substring)) { return; } await driver.pause(pollMs); diff --git a/appium/tests/helpers/selectors.ts b/appium/tests/helpers/selectors.ts index c773492..78e73e8 100644 --- a/appium/tests/helpers/selectors.ts +++ b/appium/tests/helpers/selectors.ts @@ -35,21 +35,73 @@ export function getTestExternalId(): string { return `appium-${sdk}-${platform}`; } -const TEST_DATA: Record = { - 'appium-flutter-ios': { sms: '+12003004000', email: 'flutter-ios@test.com' }, - 'appium-flutter-android': { sms: '+12003004001', email: 'flutter-android@test.com' }, - 'appium-react-native-ios': { sms: '+12003004002', email: 'rn-ios@test.com' }, - 'appium-react-native-android': { sms: '+12003004003', email: 'rn-android@test.com' }, - 'appium-capacitor-ios': { sms: '+12003004004', email: 'capacitor-ios@test.com' }, - 'appium-capacitor-android': { sms: '+12003004005', email: 'capacitor-android@test.com' }, - 'appium-cordova-ios': { sms: '+12003004006', email: 'cordova-ios@test.com' }, - 'appium-cordova-android': { sms: '+12003004007', email: 'cordova-android@test.com' }, - 'appium-unity-ios': { sms: '+12003004008', email: 'unity-ios@test.com' }, - 'appium-unity-android': { sms: '+12003004009', email: 'unity-android@test.com' }, - 'appium-dotnet-ios': { sms: '+12003004010', email: 'dotnet-ios@test.com' }, - 'appium-dotnet-android': { sms: '+12003004011', email: 'dotnet-android@test.com' }, - 'appium-ios': { sms: '+12003004012', email: 'ios@test.com' }, - 'appium-android': { sms: '+12003004013', email: 'android@test.com' }, +const TEST_DATA: Record = { + 'appium-flutter-ios': { + sms: '+12003004000', + email: 'flutter-ios@test.com', + customEvent: 'flutter_ios', + }, + 'appium-flutter-android': { + sms: '+12003004001', + email: 'flutter-android@test.com', + customEvent: 'flutter_android', + }, + 'appium-react-native-ios': { + sms: '+12003004002', + email: 'rn-ios@test.com', + customEvent: 'rn_ios', + }, + 'appium-react-native-android': { + sms: '+12003004003', + email: 'rn-android@test.com', + customEvent: 'rn_android', + }, + 'appium-capacitor-ios': { + sms: '+12003004004', + email: 'capacitor-ios@test.com', + customEvent: 'capacitor_ios', + }, + 'appium-capacitor-android': { + sms: '+12003004005', + email: 'capacitor-android@test.com', + customEvent: 'capacitor_android', + }, + 'appium-cordova-ios': { + sms: '+12003004006', + email: 'cordova-ios@test.com', + customEvent: 'cordova_ios', + }, + 'appium-cordova-android': { + sms: '+12003004007', + email: 'cordova-android@test.com', + customEvent: 'cordova_android', + }, + 'appium-unity-ios': { + sms: '+12003004008', + email: 'unity-ios@test.com', + customEvent: 'unity_ios', + }, + 'appium-unity-android': { + sms: '+12003004009', + email: 'unity-android@test.com', + customEvent: 'unity_android', + }, + 'appium-dotnet-ios': { + sms: '+12003004010', + email: 'dotnet-ios@test.com', + customEvent: 'dotnet_ios', + }, + 'appium-dotnet-android': { + sms: '+12003004011', + email: 'dotnet-android@test.com', + customEvent: 'dotnet_android', + }, + 'appium-ios': { sms: '+12003004012', email: 'ios@test.com', customEvent: 'ios' }, + 'appium-android': { + sms: '+12003004013', + email: 'android@test.com', + customEvent: 'android', + }, }; export function getTestData() { diff --git a/appium/tests/specs/1_user.spec.ts b/appium/tests/specs/01_user.spec.ts similarity index 100% rename from appium/tests/specs/1_user.spec.ts rename to appium/tests/specs/01_user.spec.ts diff --git a/appium/tests/specs/2_push.spec.ts b/appium/tests/specs/02_push.spec.ts similarity index 100% rename from appium/tests/specs/2_push.spec.ts rename to appium/tests/specs/02_push.spec.ts index 66a732b..28ea7ef 100644 --- a/appium/tests/specs/2_push.spec.ts +++ b/appium/tests/specs/02_push.spec.ts @@ -6,6 +6,11 @@ describe('Push Subscription', () => { await scrollToEl('push_section'); }); + it('should show correct tooltip info', async () => { + await checkTooltip('push_info_icon', 'push'); + await checkTooltip('send_push_info_icon', 'sendPushNotification'); + }); + it('should have push ID and be enabled initially', async () => { const pushIdEl = await scrollToEl('push_id_value'); const pushId = await pushIdEl.getText(); @@ -17,11 +22,6 @@ describe('Push Subscription', () => { expect(value).toBe('1'); }); - it('should show correct tooltip info', async () => { - await checkTooltip('push_info_icon', 'push'); - await checkTooltip('send_push_info_icon', 'sendPushNotification'); - }); - it('can send an image notification', async () => { await checkNotification({ buttonId: 'send_image_button', diff --git a/appium/tests/specs/3_iam.spec.ts b/appium/tests/specs/03_iam.spec.ts similarity index 100% rename from appium/tests/specs/3_iam.spec.ts rename to appium/tests/specs/03_iam.spec.ts diff --git a/appium/tests/specs/4_alias.spec.ts b/appium/tests/specs/04_alias.spec.ts similarity index 100% rename from appium/tests/specs/4_alias.spec.ts rename to appium/tests/specs/04_alias.spec.ts diff --git a/appium/tests/specs/5_email.spec.ts b/appium/tests/specs/05_email.spec.ts similarity index 94% rename from appium/tests/specs/5_email.spec.ts rename to appium/tests/specs/05_email.spec.ts index f678b08..75499ea 100644 --- a/appium/tests/specs/5_email.spec.ts +++ b/appium/tests/specs/05_email.spec.ts @@ -29,7 +29,7 @@ describe('Emails', () => { await el.waitForDisplayed({ timeout: 5_000 }); // remove email - const removeButton = await byTestId(`remove_${email}`); + const removeButton = await byTestId(`emails_remove_${email}`); await removeButton.click(); el = await byText(email); diff --git a/appium/tests/specs/6_sms.spec.ts b/appium/tests/specs/06_sms.spec.ts similarity index 94% rename from appium/tests/specs/6_sms.spec.ts rename to appium/tests/specs/06_sms.spec.ts index 1d9d8bc..ac305b0 100644 --- a/appium/tests/specs/6_sms.spec.ts +++ b/appium/tests/specs/06_sms.spec.ts @@ -28,7 +28,7 @@ describe('SMS', () => { await el.waitForDisplayed({ timeout: 5_000 }); // remove sms - const removeButton = await byTestId(`remove_${sms}`); + const removeButton = await byTestId(`sms_remove_${sms}`); await removeButton.click(); el = await byText(sms); diff --git a/appium/tests/specs/7_tag.spec.ts b/appium/tests/specs/07_tag.spec.ts similarity index 93% rename from appium/tests/specs/7_tag.spec.ts rename to appium/tests/specs/07_tag.spec.ts index 1276b7a..e129060 100644 --- a/appium/tests/specs/7_tag.spec.ts +++ b/appium/tests/specs/07_tag.spec.ts @@ -16,14 +16,14 @@ describe('Tags', () => { await addButton.click(); // add tag - const keyInput = await byTestId('multi_pair_key_0'); + const keyInput = await byTestId('tag_key_input'); await keyInput.waitForDisplayed({ timeout: 5_000 }); await keyInput.setValue('test_tag'); - const valueInput = await byTestId('multi_pair_value_0'); + const valueInput = await byTestId('tag_value_input'); await valueInput.setValue('test_tag_value'); - const confirmButton = await byTestId('multi_pair_confirm_button'); + const confirmButton = await byTestId('tag_confirm_button'); await confirmButton.click(); await expectPairInSection('tags', 'test_tag', 'test_tag_value'); diff --git a/appium/tests/specs/08_outcome.spec.ts b/appium/tests/specs/08_outcome.spec.ts new file mode 100644 index 0000000..cfdcf18 --- /dev/null +++ b/appium/tests/specs/08_outcome.spec.ts @@ -0,0 +1,72 @@ +import { checkTooltip, scrollToEl, waitForAppReady } from '../helpers/app'; +import { byTestId, byText } from '../helpers/selectors.js'; + +describe('Outcomes', () => { + before(async () => { + await waitForAppReady(); + await scrollToEl('outcomes_section'); + }); + + it('should show correct tooltip info', async () => { + await checkTooltip('outcomes_info_icon', 'outcomes'); + }); + + it('can send a normal outcome', async () => { + const sendButton = await scrollToEl('SEND OUTCOME', { by: 'text' }); + await sendButton.click(); + + const nameInput = await byTestId('outcome_name_input'); + await nameInput.waitForDisplayed({ timeout: 5_000 }); + await nameInput.setValue('test_normal'); + + const normalRadio = await byText('Normal Outcome'); + await normalRadio.click(); + + const sendBtn = await byTestId('outcome_send_button'); + await sendBtn.click(); + + const snackbar = await byText('Outcome sent: test_normal'); + await snackbar.waitForDisplayed({ timeout: 5_000 }); + }); + + it('can send a unique outcome', async () => { + const sendButton = await scrollToEl('SEND OUTCOME', { by: 'text' }); + await sendButton.click(); + + const nameInput = await byTestId('outcome_name_input'); + await nameInput.waitForDisplayed({ timeout: 5_000 }); + await nameInput.setValue('test_unique'); + + const uniqueRadio = await byText('Unique Outcome'); + await uniqueRadio.click(); + + const sendBtn = await byTestId('outcome_send_button'); + await sendBtn.click(); + + const snackbar = await byText('Unique outcome sent: test_unique'); + await snackbar.waitForDisplayed({ timeout: 5_000 }); + }); + + it('can send an outcome with value', async () => { + const sendButton = await scrollToEl('SEND OUTCOME', { by: 'text' }); + await sendButton.click(); + + const nameInput = await byTestId('outcome_name_input'); + await nameInput.waitForDisplayed({ timeout: 5_000 }); + + const withValueRadio = await byText('Outcome with Value'); + await withValueRadio.click(); + + await nameInput.setValue('test_valued'); + + const valueInput = await byTestId('outcome_value_input'); + await valueInput.waitForDisplayed({ timeout: 5_000 }); + await valueInput.setValue('3.14'); + + const sendBtn = await byTestId('outcome_send_button'); + await sendBtn.click(); + + const snackbar = await byText('Outcome sent: test_valued = 3.14'); + await snackbar.waitForDisplayed({ timeout: 5_000 }); + }); +}); diff --git a/appium/tests/specs/09_trigger.spec.ts b/appium/tests/specs/09_trigger.spec.ts new file mode 100644 index 0000000..f369d48 --- /dev/null +++ b/appium/tests/specs/09_trigger.spec.ts @@ -0,0 +1,103 @@ +import { checkTooltip, expectPairInSection, scrollToEl, waitForAppReady } from '../helpers/app'; +import { byTestId, byText } from '../helpers/selectors.js'; + +async function addMultipleTriggers() { + const addButton = await scrollToEl('ADD MULTIPLE TRIGGERS', { by: 'text' }); + await addButton.click(); + + const addRowButton = await byText('Add Row'); + await addRowButton.click(); + + const key0 = await byTestId('Key_input_0'); + await key0.waitForDisplayed({ timeout: 5_000 }); + await key0.setValue('test_trigger_key_2'); + + const value0 = await byTestId('Value_input_0'); + await value0.setValue('test_trigger_value_2'); + + const key1 = await byTestId('Key_input_1'); + await key1.setValue('test_trigger_key_3'); + + const value1 = await byTestId('Value_input_1'); + await value1.setValue('test_trigger_value_3'); + + let confirmButton = await byText('Add All'); + await confirmButton.click(); + + await expectPairInSection('triggers', 'test_trigger_key_2', 'test_trigger_value_2'); + await expectPairInSection('triggers', 'test_trigger_key_3', 'test_trigger_value_3'); +} + +describe('Triggers', () => { + before(async () => { + await waitForAppReady(); + await scrollToEl('triggers_section'); + }); + + it('should show correct tooltip info', async () => { + await checkTooltip('triggers_info_icon', 'triggers'); + }); + + it('can add and remove trigger', async () => { + const addButton = await scrollToEl('ADD TRIGGER', { by: 'text' }); + await addButton.click(); + + // add trigger + const keyInput = await byTestId('trigger_key_input'); + await keyInput.waitForDisplayed({ timeout: 5_000 }); + await keyInput.setValue('test_trigger_key'); + + const valueInput = await byTestId('trigger_value_input'); + await valueInput.setValue('test_trigger_value'); + + const confirmButton = await byTestId('trigger_confirm_button'); + await confirmButton.click(); + + await expectPairInSection('triggers', 'test_trigger_key', 'test_trigger_value'); + + // remove trigger + const removeButton = await byTestId(`triggers_remove_test_trigger_key`); + await removeButton.click(); + + const el = await byText('test_trigger_key'); + await el.waitForDisplayed({ timeout: 5_000, reverse: true }); + }); + + it('can add multiple triggers', async () => { + await addMultipleTriggers(); + + // remove triggers + const removeButton = await scrollToEl('REMOVE TRIGGERS'); + await removeButton.click(); + + const trigger2Checkbox = await byTestId('remove_checkbox_test_trigger_key_2'); + await trigger2Checkbox.waitForDisplayed({ timeout: 5_000 }); + await trigger2Checkbox.click(); + + const trigger3Checkbox = await byTestId('remove_checkbox_test_trigger_key_3'); + await trigger3Checkbox.click(); + + const confirmButton = await byText('Remove (2)'); + await confirmButton.click(); + + await scrollToEl('triggers_section', { direction: 'up' }); + + // wait for triggers to be removed + const trigger2El = await byText('test_trigger_key_2'); + const trigger3El = await byText('test_trigger_key_3'); + await trigger2El.waitForDisplayed({ timeout: 5_000, reverse: true }); + await trigger3El.waitForDisplayed({ timeout: 5_000, reverse: true }); + }); + + it('can clear all triggers', async () => { + await addMultipleTriggers(); + + // clear all triggers + const clearButton = await scrollToEl('CLEAR ALL TRIGGERS'); + await clearButton.click(); + + await scrollToEl('triggers_section', { direction: 'up' }); + const el = await byText('No triggers added'); + await el.waitForDisplayed({ timeout: 5_000 }); + }); +}); diff --git a/appium/tests/specs/10_event.spec.ts b/appium/tests/specs/10_event.spec.ts new file mode 100644 index 0000000..816d169 --- /dev/null +++ b/appium/tests/specs/10_event.spec.ts @@ -0,0 +1,67 @@ +import { scrollToEl, waitForAppReady } from '../helpers/app'; +import { byTestId, byText, getTestData } from '../helpers/selectors.js'; + +const TEST_JSON = { + someNum: 123, + someFloat: 3.14159, + someString: 'abc', + someBool: true, + someObject: { + abc: '123', + nested: { + def: '456', + }, + ghi: null, + }, + someArray: [1, 2], + someMixedArray: [1, '2', { abc: '123' }, null], + someNull: null, +}; + +describe('Custom Events', () => { + before(async () => { + await waitForAppReady(); + await scrollToEl('custom_events_section'); + }); + + // wait for rename when merged to main + // it('should show correct tooltip info', async () => { + // await checkTooltip('custom_events_info_icon', 'trackEvent'); + // }); + + it('can send a custom event with no properties', async () => { + const { customEvent } = getTestData(); + const sendButton = await scrollToEl('TRACK EVENT', { by: 'text' }); + await sendButton.click(); + + const nameInput = await byTestId('event_name_input'); + await nameInput.waitForDisplayed({ timeout: 5_000 }); + await nameInput.setValue(`${customEvent}_no_props`); + + const trackBtn = await byTestId('event_track_button'); + await trackBtn.click(); + + const snackbar = await byText(`Event tracked: ${customEvent}_no_props`); + await snackbar.waitForDisplayed({ timeout: 5_000 }); + }); + + it('can send a custom event with properties', async () => { + const { customEvent } = getTestData(); + const sendButton = await scrollToEl('TRACK EVENT', { by: 'text' }); + await sendButton.click(); + + const nameInput = await byTestId('event_name_input'); + await nameInput.waitForDisplayed({ timeout: 5_000 }); + await nameInput.setValue(`${customEvent}_with_props`); + + const propertiesInput = await byTestId('event_properties_input'); + await propertiesInput.click(); + await propertiesInput.setValue(JSON.stringify(TEST_JSON)); + + const trackBtn = await byTestId('event_track_button'); + await trackBtn.click(); + + const snackbar = await byText(`Event tracked: ${customEvent}_with_props`); + await snackbar.waitForDisplayed({ timeout: 5_000 }); + }); +}); diff --git a/appium/tests/specs/11_location.spec.ts b/appium/tests/specs/11_location.spec.ts new file mode 100644 index 0000000..9b00a98 --- /dev/null +++ b/appium/tests/specs/11_location.spec.ts @@ -0,0 +1,45 @@ +import { waitForAppReady, waitForAlert, scrollToEl, checkTooltip } from '../helpers/app.js'; +import { byText } from '../helpers/selectors.js'; + +describe('Location', () => { + before(async () => { + await waitForAppReady({ skipLogin: true }); + await scrollToEl('location_section'); + }); + + it('can show correct tooltip info', async () => { + await checkTooltip('location_info_icon', 'location'); + }); + + it('can prompt for location', async () => { + const promptButton = await scrollToEl('PROMPT LOCATION', { by: 'text' }); + await promptButton.click(); + + await driver.pause(3_000); + const alert = await waitForAlert(); + + expect(alert).toContain('location'); + await driver.execute('mobile: alert', { + action: 'accept', + buttonLabel: 'Allow While Using App', + }); + }); + + it('can share location', async () => { + let checkSharedButton = await scrollToEl('CHECK LOCATION SHARED', { by: 'text' }); + await checkSharedButton.click(); + + let snackbar = await byText('Location shared: false'); + await snackbar.waitForDisplayed({ timeout: 5_000 }); + + // toggle location sharing on + const shareButton = await scrollToEl('Share device location', { by: 'text', partial: true }); + await shareButton.click(); + + // verify it's now shared — re-fetch to avoid stale reference after scroll + checkSharedButton = await scrollToEl('CHECK LOCATION SHARED', { by: 'text' }); + await checkSharedButton.click(); + snackbar = await byText('Location shared: true'); + await snackbar.waitForDisplayed({ timeout: 5_000 }); + }); +}); diff --git a/appium/tests/specs/12_activity.spec.ts b/appium/tests/specs/12_activity.spec.ts new file mode 100644 index 0000000..7fe161d --- /dev/null +++ b/appium/tests/specs/12_activity.spec.ts @@ -0,0 +1,74 @@ +import { + waitForAppReady, + scrollToEl, + checkTooltip, + lockScreen, + returnToApp, +} from '../helpers/app.js'; +import { getPlatform } from '../helpers/selectors.js'; + +async function checkActivity(options: { orderId?: string; status: string; message: string }) { + const { orderId = 'ORD-1234', status, message } = options; + + await lockScreen(); + + const statusEl = await $(`-ios predicate string:label CONTAINS "${status}"`); + await statusEl.waitForDisplayed({ timeout: 10_000 }); + + const messageEl = await $(`-ios predicate string:label CONTAINS "${message}"`); + await expect(messageEl).toBeDisplayed(); + + const orderEl = await $(`-ios predicate string:label CONTAINS "${orderId}"`); + await expect(orderEl).toBeDisplayed(); + + await returnToApp(); +} + +describe('Live Activities', () => { + before(async function () { + if (getPlatform() !== 'ios') { + return this.skip(); + } + await waitForAppReady({ skipLogin: true }); + await scrollToEl('live_activities_section'); + }); + + it('can show correct tooltip info', async () => { + await checkTooltip('live_activities_info_icon', 'liveActivities'); + }); + + it('can start a live, update, and exit activity', async () => { + const startButton = await scrollToEl('START LIVE ACTIVITY', { by: 'text' }); + await startButton.click(); + + const clickUpdateButton = async (status: string) => { + let updateButton = await scrollToEl(`UPDATE → ${status}`, { by: 'text' }); + await updateButton.click(); + await driver.pause(3_000); + }; + + await checkActivity({ + status: 'Preparing', + message: 'Your order is being prepared', + }); + + // update live activity to on the way + await clickUpdateButton('ON THE WAY'); + + await checkActivity({ + status: 'On the Way', + message: 'Driver is heading your way', + }); + + // end live activity + const endButton = await scrollToEl('END LIVE ACTIVITY', { by: 'text' }); + await endButton.click(); + await driver.pause(3_000); + await lockScreen(); + + const activityEl = await $(`-ios predicate string:label CONTAINS "ORD-1234"`); + await activityEl.waitForDisplayed({ timeout: 5_000, reverse: true }); + + await returnToApp(); + }); +}); diff --git a/appium/wdio.android.conf.ts b/appium/wdio.android.conf.ts index 7940ad9..06db0a9 100644 --- a/appium/wdio.android.conf.ts +++ b/appium/wdio.android.conf.ts @@ -8,11 +8,11 @@ export const config: WebdriverIO.Config = { { platformName: 'Android', 'appium:app': isLocal ? process.env.APP_PATH : process.env.BROWSERSTACK_APP_URL, - 'appium:deviceName': process.env.DEVICE || 'Google Pixel 8', - 'appium:platformVersion': process.env.OS_VERSION || '14', + 'appium:deviceName': process.env.DEVICE || 'Samsung Galaxy S26', + 'appium:platformVersion': process.env.OS_VERSION || '16', 'appium:automationName': 'UiAutomator2', ...(process.env.BUNDLE_ID ? { 'appium:appPackage': process.env.BUNDLE_ID } : {}), - 'appium:autoGrantPermissions': true, + 'appium:autoGrantPermissions': false, 'appium:noReset': true, ...(isLocal ? {} : { 'bstack:options': bstackOptions }), }, diff --git a/appium/wdio.ios.conf.ts b/appium/wdio.ios.conf.ts index a3d9425..7056ebc 100644 --- a/appium/wdio.ios.conf.ts +++ b/appium/wdio.ios.conf.ts @@ -12,7 +12,7 @@ export const config: WebdriverIO.Config = { 'appium:platformVersion': process.env.OS_VERSION || '18', 'appium:automationName': 'XCUITest', ...(process.env.BUNDLE_ID ? { 'appium:bundleId': process.env.BUNDLE_ID } : {}), - 'appium:autoAcceptAlerts': true, + 'appium:autoAcceptAlerts': false, 'appium:noReset': true, ...(isLocal ? {} : { 'bstack:options': bstackOptions }), }, diff --git a/demo/tooltip_content.json b/demo/tooltip_content.json index e2733fe..28d7adb 100644 --- a/demo/tooltip_content.json +++ b/demo/tooltip_content.json @@ -35,6 +35,10 @@ "title": "Location", "description": "Share device location (requires user permission) for location-based segmentation and messaging." }, + "customEvents": { + "title": "Custom Events", + "description": "Send custom events with optional properties to trigger Journeys, activate Wait Until steps, and power real-time personalized messaging." + }, "trackEvent": { "title": "Track Event", "description": "Send custom events with optional properties to trigger Journeys, activate Wait Until steps, and power real-time personalized messaging."