Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0806181
test(appium): add outcome sending tests
fadi-george Apr 14, 2026
d3e0baa
test(appium): add custom events testing support
fadi-george Apr 14, 2026
2ee075b
test(appium): add triggers tests and update tag selectors
fadi-george Apr 14, 2026
b1f6be3
test(appium): replace UI log helpers with native logs
fadi-george Apr 14, 2026
7e36fac
test(appium): disable auto alert handling for iOS and add location tests
fadi-george Apr 14, 2026
d6301d9
test(appium): add live activities tests
fadi-george Apr 14, 2026
c9afce8
test(appium): refactor live activity test timing
fadi-george Apr 14, 2026
2e82f7f
test(appium): improve live activity test stability
fadi-george Apr 14, 2026
9b4f6ac
test(appium): extract returnToApp helper function
fadi-george Apr 14, 2026
1660ea9
test(appium): rename events spec file to event
fadi-george Apr 14, 2026
9d38c1d
test(appium): switch event tests to use UI assertions
fadi-george Apr 14, 2026
2a06f12
test(appium): add zero-padding to spec file names
fadi-george Apr 14, 2026
500ca56
test(appium): improve scroll element positioning
fadi-george Apr 14, 2026
ccb8970
test(appium): add new test specs and fix typo
fadi-george Apr 14, 2026
1ade972
test(appium): update test IDs and fix scroll bug
fadi-george Apr 14, 2026
f5ee1a1
test(appium): add Android support and device flag
fadi-george Apr 14, 2026
8936c36
test(appium): refactor iOS screen/app helpers
fadi-george Apr 14, 2026
4409067
test(appium): optimize scroll behavior and distance
fadi-george Apr 14, 2026
04cf52e
test(appium): update remove button test IDs
fadi-george Apr 14, 2026
b341bba
test(appium): update Android defaults and fix test issues
fadi-george Apr 14, 2026
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
23 changes: 14 additions & 9 deletions appium/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
7 changes: 3 additions & 4 deletions appium/scripts/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
54 changes: 30 additions & 24 deletions appium/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
claude[bot] marked this conversation as resolved.

Expand All @@ -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:
Expand All @@ -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

Expand Down
47 changes: 38 additions & 9 deletions appium/scripts/run-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;;
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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}"
Expand All @@ -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 ────────────────────────────────────────────────────────
Expand All @@ -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 ─────────────────────────────────────────────────────────────
Comment on lines +141 to 144
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The Android DEVICE default in run-local.sh was set to 'Android 16' — an OS version codename — instead of a valid device name; this cascades to AVD_NAME='Android_16' via the spaces-to-underscores substitution, causing emulator -avd Android_16 to immediately fail with 'AVD not found' for any developer running ./run-local.sh --platform=android without explicitly setting --device. Fix by changing the default to a valid device name such as 'Samsung Galaxy S26' (matching wdio.android.conf.ts) and updating the --help text on line 56 which still documents the old 'Google Pixel 8' default, creating a direct contradiction in the same file.

Extended reasoning...

What the bug is and how it manifests

In run-local.sh lines 139–141, the Android platform defaults block was changed from DEVICE=Google Pixel 8 / AVD_NAME=Pixel_8 to DEVICE=Android 16 / AVD_NAME=${DEVICE// /_}. 'Android 16' is an Android OS version codename (analogous to 'Android Pie' or 'Android Oreo'), not a device model or AVD name. No physical device, BrowserStack device, or locally-created AVD would ever be named 'Android 16'.

The specific code path that triggers it

The substitution AVD_NAME="${AVD_NAME:-${DEVICE// /_}}" converts spaces to underscores, producing AVD_NAME='Android_16'. The start_android_emulator() function then executes emulator -avd "Android_16". Since no standard Android SDK AVD is named 'Android_16', the emulator command exits immediately with 'PANIC: Missing emulator engine program for 'x86_64' CPU' or 'AVD 'Android_16' not found'. Any developer running ./run-local.sh --platform=android without explicitly setting --device hits this failure on the very first step after the build, before any test can run.

Why existing code doesn't prevent it

There is no validation that checks whether DEVICE is a plausible device model name versus an OS version string. The shell parameter expansion ${DEVICE// /_} mechanically replaces spaces and trusts the value is a valid device name. The companion wdio.android.conf.ts defaults to 'Samsung Galaxy S26', which shows the intended target device, but run-local.sh was not updated consistently.

The help text contradiction

The --help output (line 56, introduced in this PR) reads --device=NAME Device/simulator/AVD name (default: iPhone 17 / Google Pixel 8), still documenting the old 'Google Pixel 8' default. A developer reading --help before running the script will see 'Google Pixel 8' documented as the Android default, but the actual behavior uses 'Android 16'. This double inconsistency (wrong default + outdated help text) makes diagnosis harder.

Impact

Every developer who runs ./run-local.sh --platform=android without explicitly passing --device will receive an immediate emulator failure. Since Android support was just added by this PR (the previous code hard-errored on Android entirely), every developer attempting to use the new Android feature for the first time will hit this on their first attempt. The failure also affects BrowserStack paths if DEVICE is not set, since 'Android 16' is not a valid BrowserStack device name either.

How to fix it

Change the default from 'Android 16' to a valid device name — 'Samsung Galaxy S26' matches the wdio.android.conf.ts default and is a real BrowserStack device. The AVD_NAME default can remain dynamic (${DEVICE// /_}), but should also document a real AVD name in the .env.example comment. The --help text on line 56 must be updated to read 'iPhone 17 / Samsung Galaxy S26'.

Step-by-step proof

  1. Developer clones repo, sets ONESIGNAL credentials in .env, runs ./run-local.sh --platform=android --sdk=flutter.
  2. Script reaches the platform defaults block at line 139: DEVICE="${DEVICE:-Android 16}" — DEVICE is unset, so DEVICE='Android 16'.
  3. Line 141: AVD_NAME="${AVD_NAME:-${DEVICE// /_}}" — AVD_NAME='Android_16'.
  4. After building the APK, start_android_emulator() runs: emulator -avd "Android_16" -no-audio -no-boot-anim.
  5. Android SDK emulator reports 'PANIC: Cannot find AVD system path. Please define ANDROID_SDK_ROOT' or equivalently 'AVD Android_16 not found'. The script fails here.
  6. No tests run. The developer sees a confusing error that looks like an SDK setup issue rather than a wrong default value.
  7. Reading --help shows 'Google Pixel 8' as the documented Android default, adding confusion since the script clearly used 'Android 16' based on the error message.

Expand Down Expand Up @@ -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" <<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

info "Installing Flutter dependencies..."
(cd "$FLUTTER_DIR" && flutter pub get)

info "Building debug APK (this may take a few minutes)..."
(cd "$DEMO_DIR" && flutter build apk --debug)

[[ -f "$APP_PATH" ]] || error ".apk not found after build at $APP_PATH"
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 All @@ -173,7 +198,11 @@ build_app() {
return
fi

build_flutter_ios
if [[ "$PLATFORM" == "ios" ]]; then
build_flutter_ios
else
build_flutter_android
fi
}

# ── 2. Start device ──────────────────────────────────────────────────────────
Expand Down
92 changes: 62 additions & 30 deletions appium/tests/helpers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async function swipeMainContent(
direction: 'up' | 'down',
distance: 'small' | 'normal' | 'large' = 'normal',
) {
const distances = { small: 0.3, normal: 0.5, large: 1.0 };
const distances = { small: 0.2, normal: 0.5, large: 1.0 };
const mainScroll = await byTestId('main_scroll_view');
const platform = getPlatform();
const invertedDirection = direction === 'up' ? 'down' : 'up';
Expand Down Expand Up @@ -88,25 +88,53 @@ export async function scrollToEl(
if (await el.isDisplayed()) {
return el;
}
await swipeMainContent(direction, 'normal');
await swipeMainContent(direction, 'small');
}
throw new Error(`Element "${identifier}" not found after ${maxScrolls} scrolls`);
}

/**
* Wait for a native system alert to appear and return its text.
* Returns null if no alert appears within the timeout.
*/
export async function waitForAlert(timeoutMs = 10_000): Promise<string | null> {
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);
}
Comment on lines +127 to +132
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 On Android, waitForAppReady() calls waitForAlert() unconditionally, which internally issues driver.execute('mobile: alert', { action: 'getButtons' }) — an XCUITest-only command. On Android/UiAutomator2 this throws on every poll, causing driver.waitUntil to spin for the full 10-second timeout before returning null. Since wdio.android.conf.ts already sets autoGrantPermissions: true, no permission alert will ever appear on Android — the wait is entirely wasted. Fix: guard the waitForAlert block with if (getPlatform() === 'ios') { ... }; getPlatform() is already imported in app.ts.

Extended reasoning...

What the bug is and how it manifests

In waitForAppReady() (app.ts lines 136–141), when alertHandled is not yet set in sharedStore (i.e., first test of an Android session), waitForAlert(timeoutMs=10_000) is called with no platform check. Inside waitForAlert(), the poll function executes driver.execute('mobile: alert', { action: 'getButtons' }). This is an XCUITest-specific Appium command — on Android/UiAutomator2 it is not supported and throws an error on every invocation. The inner catch silently returns false, so driver.waitUntil continues polling at 250ms intervals until the full 10-second timeout elapses, at which point the outer catch returns null. The net result is a guaranteed ~10-second dead wait injected into every Android test session.

The specific code path that triggers it

// app.ts ~line 136
const alertHandled = await browser.sharedStore.get('alertHandled');
if (!alertHandled) {
  const alert = await waitForAlert();   // <-- spins 10s on Android
  if (alert) await driver.acceptAlert();
  await browser.sharedStore.set('alertHandled', true);
}

Inside waitForAlert():

const buttons = await driver.execute('mobile: alert', { action: 'getButtons' });
// ↑ throws NotImplementedError on UiAutomator2 every 250ms for 10 seconds

Why existing code doesn't prevent it

The alertHandled guard in sharedStore means this stall occurs once per Android session (not per spec file), but it is still 10 seconds of dead time on every run. There is no platform check before calling waitForAlert(). The inner catch in the poll function is intentionally silent (to handle the case where no alert exists on iOS), but it also silently swallows the Android-incompatible command failure on every poll.

What the impact would be

Every Android test session starts with a 10-second stall in waitForAppReady(). Since autoGrantPermissions: true is already set in wdio.android.conf.ts, no permission alert will ever appear on Android — making the wait completely pointless in addition to being slow. In CI environments running Android against BrowserStack, this wastes 10 seconds of billable device time per session.

How to fix it

Add a platform guard around the waitForAlert block. getPlatform() is already imported in app.ts (line 5) and used elsewhere in the same file (lines 45, 230, 255):

const alertHandled = await browser.sharedStore.get('alertHandled');
if (!alertHandled) {
  if (getPlatform() === 'ios') {
    const alert = await waitForAlert();
    if (alert) await driver.acceptAlert();
  }
  await browser.sharedStore.set('alertHandled', true);
}

Step-by-step proof

  1. Android session starts; first spec file calls waitForAppReady().
  2. browser.sharedStore.get('alertHandled') returns null (not yet set).
  3. waitForAlert(10_000) is called. Inside, driver.waitUntil begins polling every 250ms.
  4. Each poll executes driver.execute('mobile: alert', { action: 'getButtons' }). UiAutomator2 does not implement this command and throws Error: Not yet implemented (or similar) on every call.
  5. The inner catch returns false on every poll. driver.waitUntil never sees a true and runs for the full 10,000ms before throwing a timeout error.
  6. The outer catch in waitForAlert() returns null.
  7. Back in waitForAppReady(), alert is null so driver.acceptAlert() is skipped. alertHandled is set to true.
  8. Total wasted time: ~10 seconds. The first Android test now begins 10 seconds late.


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' });
Expand Down Expand 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();
}

Expand All @@ -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<string, unknown>;
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);
}

/**
Expand All @@ -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;
Expand Down Expand Up @@ -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<string, unknown>;
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([
{
Expand Down Expand Up @@ -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: {
Expand Down
Loading
Loading