-
Notifications
You must be signed in to change notification settings - Fork 0
test: [SDK-4333] expand Appium E2E test coverage #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0806181
d3e0baa
2ee075b
b1f6be3
7e36fac
d6301d9
c9afce8
2e82f7f
9b4f6ac
1660ea9
9d38c1d
2a06f12
500ca56
ccb8970
1ade972
f5ee1a1
8936c36
4409067
04cf52e
b341bba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 ───────────────────────────────────────────────────────────── | ||
|
Comment on lines
+141
to
144
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 The specific code path that triggers it The substitution 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 The help text contradiction The Impact Every developer who runs 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 Step-by-step proof
|
||
|
|
@@ -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 | ||
|
|
@@ -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 ────────────────────────────────────────────────────────── | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 On Android, Extended reasoning...What the bug is and how it manifests In 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 const buttons = await driver.execute('mobile: alert', { action: 'getButtons' });
// ↑ throws NotImplementedError on UiAutomator2 every 250ms for 10 secondsWhy existing code doesn't prevent it The What the impact would be Every Android test session starts with a 10-second stall in How to fix it Add a platform guard around the 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
|
||
|
|
||
| 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<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); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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<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([ | ||
| { | ||
|
|
@@ -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: { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.