From 98e617f173afa3072d66ad204f454ffc912f3a82 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 16:21:12 -0700 Subject: [PATCH 01/21] test: improve system alert handling in Appium test(appium): add Flutter Android support and improve cross-platform compatibility --- appium/tests/helpers/app.ts | 124 ++++++++++++++++++++++++++---- appium/tests/helpers/selectors.ts | 91 +++++++++++++++++----- 2 files changed, 180 insertions(+), 35 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index ab1192e..a5f5a64 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { byTestId, byText, getPlatform, getTestExternalId } from './selectors.js'; +import { byTestId, byText, getPlatform, getSdkType, getTestExternalId } from './selectors.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const tooltipContent = JSON.parse( @@ -10,11 +10,22 @@ const tooltipContent = JSON.parse( ); async function stopScrolling() { - const mainScroll = await byTestId('main_scroll_view'); - const loc = await mainScroll.getLocation(); - const size = await mainScroll.getSize(); - const x = Math.round(loc.x + 6); - const y = Math.round(loc.y + size.height / 2); + const platform = getPlatform(); + + let x: number; + let y: number; + + if (platform === 'ios') { + const mainScroll = await byTestId('main_scroll_view'); + const loc = await mainScroll.getLocation(); + const size = await mainScroll.getSize(); + x = Math.round(loc.x + 6); + y = Math.round(loc.y + size.height / 2); + } else { + const { width, height } = await driver.getWindowSize(); + x = Math.round(width / 2); + y = Math.round(height / 2); + } await driver.performActions([ { @@ -41,15 +52,18 @@ async function swipeMainContent( distance: 'small' | 'normal' | 'large' = 'normal', ) { 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'; if (platform === 'ios') { await driver.execute('mobile: swipe', { direction: invertedDirection }); } else { + const { width, height } = await driver.getWindowSize(); await driver.execute('mobile: scrollGesture', { - elementId: mainScroll.elementId, + left: 0, + top: Math.round(height * 0.1), + width, + height: Math.round(height * 0.8), direction: direction === 'up' ? 'down' : 'up', percent: distances[distance], }); @@ -94,8 +108,10 @@ export async function scrollToEl( } /** - * Wait for a native system alert to appear and return its text. - * Returns null if no alert appears within the timeout. + * Wait for an iOS system alert to appear and return its text without + * dismissing it. Returns null if no alert appears within the timeout. + * iOS-only — used by the location spec which needs to accept with a + * specific button label. */ export async function waitForAlert(timeoutMs = 10_000): Promise { try { @@ -116,21 +132,79 @@ export async function waitForAlert(timeoutMs = 10_000): Promise { } } +/** + * Wait for a native system alert/permission dialog, accept it, and return + * its text. Returns null if no dialog appears within the timeout. + * + * iOS: uses XCUITest `mobile: alert` API. + * Android: looks for the standard permission dialog "Allow" button via + * UiAutomator (works for POST_NOTIFICATIONS, location, etc.). + */ +export async function acceptSystemAlert(timeoutMs = 10_000): Promise { + const platform = getPlatform(); + + try { + if (platform === 'ios') { + const text = await waitForAlert(timeoutMs); + if (text) await driver.acceptAlert(); + return text; + } + + const allowBtn = await $('android=new UiSelector().text("Allow")'); + await allowBtn.waitForDisplayed({ timeout: timeoutMs }); + let text = 'Permission dialog'; + try { + const msgEl = await $( + 'android=new UiSelector().resourceId("com.android.permissioncontroller:id/permission_message")', + ); + text = await msgEl.getText(); + } catch { + /* best-effort */ + } + await allowBtn.click(); + return text; + } catch { + return null; + } +} + /** * Wait for the app to fully launch and the home screen to be visible. */ export async function waitForAppReady(opts: { skipLogin?: boolean } = {}) { const { skipLogin = false } = opts; - const mainScroll = await byTestId('main_scroll_view'); - await mainScroll.waitForDisplayed({ timeout: 5_000 }); + + if (getPlatform() === 'android' && getSdkType() === 'flutter') { + await driver.updateSettings({ disableIdLocatorAutocompletion: true }); + } + + const waitForMainScroll = async () => { + const mainScroll = await byTestId('main_scroll_view'); + await mainScroll.waitForDisplayed({ timeout: 5_000 }); + }; const alertHandled = await browser.sharedStore.get('alertHandled'); if (!alertHandled) { - const alert = await waitForAlert(); - if (alert) await driver.acceptAlert(); - await browser.sharedStore.set('alertHandled', true); + // Dismiss permission dialogs until the app UI is visible + while (await acceptSystemAlert(5_000)) { + await driver.pause(500); + } + } + + const html = await driver.getPageSource(); + console.log(html); + + try { + await waitForMainScroll(); + } catch { + while (await acceptSystemAlert(2_000)) { + await driver.pause(500); + } + await waitForMainScroll(); } + 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 @@ -150,9 +224,27 @@ export async function waitForAppReady(opts: { skipLogin?: boolean } = {}) { * Tap the login button, enter an external user ID, and confirm. */ export async function loginUser(externalUserId: string) { - const loginButton = await byTestId('login_user_button'); + const loginButton = await byText('LOGIN USER'); await loginButton.click(); + const html = await driver.getPageSource(); + console.log(html); + + if (getPlatform() === 'android' && getSdkType() === 'flutter') { + const userIdInput = await byTestId('login_user_id_input'); + await userIdInput.waitForDisplayed({ timeout: 5_000 }); + await userIdInput.click(); + await driver.pause(250); + await driver.execute('mobile: type', { text: externalUserId }); + const confirmButton = await byText('Login'); + await browser.waitUntil(async () => confirmButton.isEnabled(), { + timeout: 5_000, + timeoutMsg: 'Expected Login button to enable', + }); + await confirmButton.click(); + return; + } + const userIdInput = await byTestId('login_user_id_input'); await userIdInput.waitForDisplayed({ timeout: 5_000 }); await userIdInput.setValue(externalUserId); diff --git a/appium/tests/helpers/selectors.ts b/appium/tests/helpers/selectors.ts index 78e73e8..1144d5f 100644 --- a/appium/tests/helpers/selectors.ts +++ b/appium/tests/helpers/selectors.ts @@ -143,28 +143,49 @@ export function getSdkType(): SdkType { ); } +type TextReadableElement = { + getText(): Promise; + getAttribute(name: string): Promise; +}; + +function withTextFallback(el: T): T { + if (!(getPlatform() === 'android' && getSdkType() === 'flutter')) { + return el; + } + + return new Proxy(el, { + get(target, prop, receiver) { + if (prop === 'getText') { + return async () => readElementText(target); + } + + const value = Reflect.get(target, prop, receiver); + if (typeof value === 'function') { + return value.bind(target); + } + + return value; + }, + }); +} + /** * Select an element by its cross-platform test ID. * * Native iOS uses `accessibilityIdentifier`, native Android Compose uses - * `testTag`, RN uses `testID`, and Flutter uses `Semantics(label:)` — all - * map to Appium accessibility id. Capacitor uses `data-testid` as a CSS - * attribute inside a WebView. + * `testTag`, RN uses `testID` — all map to Appium accessibility id (`~`). + * Flutter uses `Semantics(identifier:)` which maps to `accessibilityIdentifier` + * on iOS (`~`) but to `resource-id` on Android (UiAutomator selector). + * Capacitor uses `data-testid` as a CSS attribute inside a WebView. */ export async function byTestId(id: string) { const sdkType = getSdkType(); - switch (sdkType) { - case 'react-native': - case 'flutter': - case 'unity': - case 'cordova': - case 'dotnet': - case 'ios': - case 'android': - return $(`~${id}`); - case 'capacitor': - return $(`[data-testid="${id}"]`); - } + const platform = getPlatform(); + + if (sdkType === 'capacitor') return withTextFallback(await $(`[data-testid="${id}"]`)); + if (sdkType === 'flutter' && platform === 'android') return withTextFallback(await $(`id=${id}`)); + + return withTextFallback(await $(`~${id}`)); } /** @@ -176,13 +197,45 @@ export async function byText(text: string, partial = false) { const sdkType = getSdkType(); if (sdkType === 'capacitor') { - return $(`//*[contains(text(), "${text}")]`); + return withTextFallback(await $(`//*[contains(text(), "${text}")]`)); } if (platform === 'ios') { const op = partial ? 'CONTAINS' : '=='; - return $(`-ios predicate string:label ${op} "${text}"`); + return withTextFallback(await $(`-ios predicate string:label ${op} "${text}"`)); } - const method = partial ? 'textContains' : 'text'; - return $(`android=new UiSelector().${method}("${text}")`); + + if (partial) { + return withTextFallback( + await $(`//*[contains(@content-desc, "${text}") or contains(@text, "${text}")]`), + ); + } + + return withTextFallback(await $(`//*[@content-desc="${text}" or @text="${text}"]`)); +} + +export async function readElementText( + el: { + getText(): Promise; + getAttribute(name: string): Promise; + }, +): Promise { + const text = (await el.getText()).trim(); + if (text) return text; + + const attributeNames = + getPlatform() === 'ios' + ? ['label', 'name', 'value'] + : ['content-desc', 'contentDescription', 'text', 'name']; + + for (const attributeName of attributeNames) { + try { + const value = (await el.getAttribute(attributeName))?.trim(); + if (value) return value; + } catch { + /* best-effort */ + } + } + + return ''; } From 82f35f7f968688e3a31915b36d7711e7c2dac592 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 17:33:39 -0700 Subject: [PATCH 02/21] test: refactor system alert handling logic --- appium/tests/helpers/app.ts | 44 ++++++++++++++----------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index a5f5a64..c92d934 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -168,42 +168,33 @@ export async function acceptSystemAlert(timeoutMs = 10_000): Promise { + const alertText = await acceptSystemAlert(timeoutMs); + if (!alertText) return; + + await driver.pause(500); + await acceptSystemAlerts(1_000); +} + /** * Wait for the app to fully launch and the home screen to be visible. */ export async function waitForAppReady(opts: { skipLogin?: boolean } = {}) { const { skipLogin = false } = opts; - if (getPlatform() === 'android' && getSdkType() === 'flutter') { - await driver.updateSettings({ disableIdLocatorAutocompletion: true }); - } - - const waitForMainScroll = async () => { - const mainScroll = await byTestId('main_scroll_view'); - await mainScroll.waitForDisplayed({ timeout: 5_000 }); - }; + // if (getPlatform() === 'android' && getSdkType() === 'flutter') { + // await driver.updateSettings({ disableIdLocatorAutocompletion: true }); + // } const alertHandled = await browser.sharedStore.get('alertHandled'); if (!alertHandled) { - // Dismiss permission dialogs until the app UI is visible - while (await acceptSystemAlert(5_000)) { - await driver.pause(500); - } + // Accept permission dialogs until the app UI is visible. + await acceptSystemAlerts(5_000); + await browser.sharedStore.set('alertHandled', true); } - const html = await driver.getPageSource(); - console.log(html); - - try { - await waitForMainScroll(); - } catch { - while (await acceptSystemAlert(2_000)) { - await driver.pause(500); - } - await waitForMainScroll(); - } - - await browser.sharedStore.set('alertHandled', true); + const mainScroll = await byTestId('main_scroll_view'); + await mainScroll.waitForDisplayed({ timeout: 5_000 }); if (skipLogin) return; @@ -227,9 +218,6 @@ export async function loginUser(externalUserId: string) { const loginButton = await byText('LOGIN USER'); await loginButton.click(); - const html = await driver.getPageSource(); - console.log(html); - if (getPlatform() === 'android' && getSdkType() === 'flutter') { const userIdInput = await byTestId('login_user_id_input'); await userIdInput.waitForDisplayed({ timeout: 5_000 }); From e2b97db6f90f5a271aff1d84f4494b0d27134c00 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 17:52:27 -0700 Subject: [PATCH 03/21] test(appium): move Flutter Android settings to config --- appium/tests/helpers/app.ts | 4 ---- appium/wdio.android.conf.ts | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index c92d934..9df9761 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -182,10 +182,6 @@ async function acceptSystemAlerts(timeoutMs: number): Promise { export async function waitForAppReady(opts: { skipLogin?: boolean } = {}) { const { skipLogin = false } = opts; - // if (getPlatform() === 'android' && getSdkType() === 'flutter') { - // await driver.updateSettings({ disableIdLocatorAutocompletion: true }); - // } - const alertHandled = await browser.sharedStore.get('alertHandled'); if (!alertHandled) { // Accept permission dialogs until the app UI is visible. diff --git a/appium/wdio.android.conf.ts b/appium/wdio.android.conf.ts index 06db0a9..4fdacb5 100644 --- a/appium/wdio.android.conf.ts +++ b/appium/wdio.android.conf.ts @@ -15,6 +15,10 @@ export const config: WebdriverIO.Config = { 'appium:autoGrantPermissions': false, 'appium:noReset': true, ...(isLocal ? {} : { 'bstack:options': bstackOptions }), + + // Disable ID locator autocompletion to avoid Flutter's Semantics(container:true) wrapping inputs in a View. + // @ts-expect-error - Appium types are not fully compatible with WebdriverIO types + 'appium:settings[disableIdLocatorAutocompletion]': true, }, ], }; From a0b49f0ed4325977a1d843c0536f93a232ab727b Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 17:59:25 -0700 Subject: [PATCH 04/21] test(appium): refactor notification clearing logic --- appium/tests/helpers/app.ts | 18 +++--------------- appium/tests/helpers/selectors.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index 9df9761..a5b6fbe 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -311,20 +311,6 @@ export async function returnToApp() { await driver.pause(1_000); } -/** - * Clear all notifications. - * Android: uses the native clearAllNotifications command. - * iOS: taps the app's "CLEAR ALL" button since XCUITest has no equivalent. - */ -export async function clearAllNotifications() { - if (getPlatform() === 'android') { - await driver.execute('mobile: clearAllNotifications', {}); - } else { - const clearButton = await scrollToEl('clear_all_button'); - await clearButton.click(); - } -} - /** * Wait for a notification to be received. * @@ -470,7 +456,9 @@ export async function checkNotification(opts: { body?: string; expectImage?: boolean; }) { - await clearAllNotifications(); + const clearButton = await scrollToEl('clear_all_button'); + await clearButton.click(); + await driver.pause(1_000); const button = await scrollToEl(opts.buttonId); await button.click(); diff --git a/appium/tests/helpers/selectors.ts b/appium/tests/helpers/selectors.ts index 1144d5f..850b1c9 100644 --- a/appium/tests/helpers/selectors.ts +++ b/appium/tests/helpers/selectors.ts @@ -159,6 +159,25 @@ function withTextFallback(el: T): T { return async () => readElementText(target); } + if (prop === 'getAttribute') { + return async (name: string) => { + try { + const value = await target.getAttribute(name); + if (value !== null) return value; + } catch { + /* fall through to Android-specific fallback */ + } + + if (name === 'value') { + const checked = await target.getAttribute('checked'); + if (checked === 'true') return '1'; + if (checked === 'false') return '0'; + } + + return null; + }; + } + const value = Reflect.get(target, prop, receiver); if (typeof value === 'function') { return value.bind(target); From 360022a6a3400302220c1f4a694bb7d43a6ea2cc Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 18:28:37 -0700 Subject: [PATCH 05/21] test(appium): fix Android scroll handling --- appium/tests/helpers/app.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index a5b6fbe..37a13ee 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -12,20 +12,21 @@ const tooltipContent = JSON.parse( async function stopScrolling() { const platform = getPlatform(); + if (platform === 'android') { + // Android's scrollGesture already completes the gesture. A follow-up tap in + // the center of the screen can hit interactive elements like LOGIN USER. + await driver.pause(150); + return; + } + let x: number; let y: number; - if (platform === 'ios') { - const mainScroll = await byTestId('main_scroll_view'); - const loc = await mainScroll.getLocation(); - const size = await mainScroll.getSize(); - x = Math.round(loc.x + 6); - y = Math.round(loc.y + size.height / 2); - } else { - const { width, height } = await driver.getWindowSize(); - x = Math.round(width / 2); - y = Math.round(height / 2); - } + const mainScroll = await byTestId('main_scroll_view'); + const loc = await mainScroll.getLocation(); + const size = await mainScroll.getSize(); + x = Math.round(loc.x + 6); + y = Math.round(loc.y + size.height / 2); await driver.performActions([ { @@ -64,7 +65,7 @@ async function swipeMainContent( top: Math.round(height * 0.1), width, height: Math.round(height * 0.8), - direction: direction === 'up' ? 'down' : 'up', + direction, percent: distances[distance], }); } From 2359afc49c0691923e4092ff393a329e9b2c47e7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 19:19:25 -0700 Subject: [PATCH 06/21] test(appium): refactor returnToApp for cross-platform --- appium/tests/helpers/app.ts | 48 ++++++++++-------------------- appium/tests/specs/02_push.spec.ts | 2 +- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index 37a13ee..e98ffb4 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -306,9 +306,20 @@ export async function lockScreen() { */ 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 }); + const platform = getPlatform(); + + if (platform === 'android') { + await driver.pressKeyCode(4); + const appId = (caps['appPackage'] ?? caps['appium:appPackage']) as string; + if (appId) { + await driver.execute('mobile: activateApp', { appId }); + } + } else { + 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); } @@ -342,40 +353,11 @@ export async function waitForNotification(opts: { } if (expectImage) { - const location = await titleEl.getLocation(); - const size = await titleEl.getSize(); - const centerX = Math.round(location.x + size.width / 2); - const startY = Math.round(location.y + size.height / 2); - const endY = startY + 300; - - await driver.performActions([ - { - type: 'pointer', - id: 'finger1', - parameters: { pointerType: 'touch' }, - actions: [ - { type: 'pointerMove', duration: 0, x: centerX, y: startY }, - { type: 'pointerDown', button: 0 }, - { type: 'pause', duration: 100 }, - { type: 'pointerMove', duration: 300, x: centerX, y: endY }, - { type: 'pointerUp', button: 0 }, - ], - }, - ]); - await driver.releaseActions(); - await driver.pause(500); - const image = await $('//android.widget.ImageView'); await image.waitForDisplayed({ timeout: 5_000 }); } - await driver.pressKeyCode(4); - - const caps = driver.capabilities as Record; - const appId = (caps['appPackage'] ?? caps['appium:appPackage']) as string; - if (appId) { - await driver.execute('mobile: activateApp', { appId }); - } + await returnToApp(); return; } diff --git a/appium/tests/specs/02_push.spec.ts b/appium/tests/specs/02_push.spec.ts index 28ea7ef..524657e 100644 --- a/appium/tests/specs/02_push.spec.ts +++ b/appium/tests/specs/02_push.spec.ts @@ -22,7 +22,7 @@ describe('Push Subscription', () => { expect(value).toBe('1'); }); - it('can send an image notification', async () => { + it.only('can send an image notification', async () => { await checkNotification({ buttonId: 'send_image_button', title: 'Image Notification', From 9557041a14280c87b993c2d129037c31e3452f57 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 19:26:45 -0700 Subject: [PATCH 07/21] test(appium): remove text fallback proxy logic --- appium/tests/helpers/selectors.ts | 90 +++---------------------------- 1 file changed, 6 insertions(+), 84 deletions(-) diff --git a/appium/tests/helpers/selectors.ts b/appium/tests/helpers/selectors.ts index 850b1c9..35ba37c 100644 --- a/appium/tests/helpers/selectors.ts +++ b/appium/tests/helpers/selectors.ts @@ -143,51 +143,6 @@ export function getSdkType(): SdkType { ); } -type TextReadableElement = { - getText(): Promise; - getAttribute(name: string): Promise; -}; - -function withTextFallback(el: T): T { - if (!(getPlatform() === 'android' && getSdkType() === 'flutter')) { - return el; - } - - return new Proxy(el, { - get(target, prop, receiver) { - if (prop === 'getText') { - return async () => readElementText(target); - } - - if (prop === 'getAttribute') { - return async (name: string) => { - try { - const value = await target.getAttribute(name); - if (value !== null) return value; - } catch { - /* fall through to Android-specific fallback */ - } - - if (name === 'value') { - const checked = await target.getAttribute('checked'); - if (checked === 'true') return '1'; - if (checked === 'false') return '0'; - } - - return null; - }; - } - - const value = Reflect.get(target, prop, receiver); - if (typeof value === 'function') { - return value.bind(target); - } - - return value; - }, - }); -} - /** * Select an element by its cross-platform test ID. * @@ -201,10 +156,10 @@ export async function byTestId(id: string) { const sdkType = getSdkType(); const platform = getPlatform(); - if (sdkType === 'capacitor') return withTextFallback(await $(`[data-testid="${id}"]`)); - if (sdkType === 'flutter' && platform === 'android') return withTextFallback(await $(`id=${id}`)); + if (sdkType === 'capacitor') return $(`[data-testid="${id}"]`); + if (sdkType === 'flutter' && platform === 'android') return $(`id=${id}`); - return withTextFallback(await $(`~${id}`)); + return $(`~${id}`); } /** @@ -213,48 +168,15 @@ export async function byTestId(id: string) { */ export async function byText(text: string, partial = false) { const platform = getPlatform(); - const sdkType = getSdkType(); - - if (sdkType === 'capacitor') { - return withTextFallback(await $(`//*[contains(text(), "${text}")]`)); - } if (platform === 'ios') { const op = partial ? 'CONTAINS' : '=='; - return withTextFallback(await $(`-ios predicate string:label ${op} "${text}"`)); + return $(`-ios predicate string:label ${op} "${text}"`); } if (partial) { - return withTextFallback( - await $(`//*[contains(@content-desc, "${text}") or contains(@text, "${text}")]`), - ); - } - - return withTextFallback(await $(`//*[@content-desc="${text}" or @text="${text}"]`)); -} - -export async function readElementText( - el: { - getText(): Promise; - getAttribute(name: string): Promise; - }, -): Promise { - const text = (await el.getText()).trim(); - if (text) return text; - - const attributeNames = - getPlatform() === 'ios' - ? ['label', 'name', 'value'] - : ['content-desc', 'contentDescription', 'text', 'name']; - - for (const attributeName of attributeNames) { - try { - const value = (await el.getAttribute(attributeName))?.trim(); - if (value) return value; - } catch { - /* best-effort */ - } + return $(`//*[contains(@content-desc, "${text}") or contains(@text, "${text}")]`); } - return ''; + return $(`//*[@content-desc="${text}" or @text="${text}"]`); } From 7bacc6907fcdbf460dbf7e6de4e885a3660e1d71 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 19:39:20 -0700 Subject: [PATCH 08/21] test(appium): add Flutter Android getText fixes --- appium/tests/helpers/selectors.ts | 56 +++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/appium/tests/helpers/selectors.ts b/appium/tests/helpers/selectors.ts index 35ba37c..7e0039a 100644 --- a/appium/tests/helpers/selectors.ts +++ b/appium/tests/helpers/selectors.ts @@ -143,6 +143,51 @@ export function getSdkType(): SdkType { ); } +/** + * On Flutter Android, the standard WebDriver getText() often returns empty + * because Flutter writes text into content-desc / text attributes rather than + * the property that UiAutomator2's getText maps to. This proxy intercepts + * getText() and falls back to those attributes. + */ +function withFlutterAndroidFixes }>(el: T): T { + if (!(getPlatform() === 'android' && getSdkType() === 'flutter')) { + return el; + } + + return new Proxy(el, { + get(target, prop, receiver) { + if (prop === 'getText') { + return async () => { + const text = (await target.getText()).trim(); + if (text) return text; + + const attrs = ['content-desc', 'contentDescription', 'text', 'name']; + for (const attr of attrs) { + try { + const val = ( + await (target as unknown as { getAttribute(n: string): Promise }) + .getAttribute(attr) + )?.trim(); + if (val) return val; + } catch { + /* best-effort */ + } + } + + return ''; + }; + } + + const value = Reflect.get(target, prop, receiver); + if (typeof value === 'function') { + return value.bind(target); + } + + return value; + }, + }); +} + /** * Select an element by its cross-platform test ID. * @@ -157,7 +202,8 @@ export async function byTestId(id: string) { const platform = getPlatform(); if (sdkType === 'capacitor') return $(`[data-testid="${id}"]`); - if (sdkType === 'flutter' && platform === 'android') return $(`id=${id}`); + if (sdkType === 'flutter' && platform === 'android') + return withFlutterAndroidFixes(await $(`id=${id}`)); return $(`~${id}`); } @@ -175,8 +221,12 @@ export async function byText(text: string, partial = false) { } if (partial) { - return $(`//*[contains(@content-desc, "${text}") or contains(@text, "${text}")]`); + return withFlutterAndroidFixes( + await $(`//*[contains(@content-desc, "${text}") or contains(@text, "${text}")]`), + ); } - return $(`//*[@content-desc="${text}" or @text="${text}"]`); + return withFlutterAndroidFixes( + await $(`//*[@content-desc="${text}" or @text="${text}"]`), + ); } From 0bc2771f6357a28599d57d9ab632ff93689b7c69 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 20:06:35 -0700 Subject: [PATCH 09/21] test(appium): improve alert handling and stability --- appium/tests/helpers/app.ts | 12 +++++++----- appium/tests/specs/01_user.spec.ts | 7 +++++++ appium/wdio.android.conf.ts | 3 +++ appium/wdio.ios.conf.ts | 3 +++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index e98ffb4..4dff8f2 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -170,11 +170,13 @@ export async function acceptSystemAlert(timeoutMs = 10_000): Promise { - const alertText = await acceptSystemAlert(timeoutMs); - if (!alertText) return; - - await driver.pause(500); - await acceptSystemAlerts(1_000); + await browser.waitUntil( + async () => { + const alertText = await acceptSystemAlert(500); + return !alertText; + }, + { timeout: timeoutMs, interval: 500 }, + ); } /** diff --git a/appium/tests/specs/01_user.spec.ts b/appium/tests/specs/01_user.spec.ts index 08c4d00..726c4f9 100644 --- a/appium/tests/specs/01_user.spec.ts +++ b/appium/tests/specs/01_user.spec.ts @@ -7,6 +7,12 @@ describe('User', () => { await scrollToEl('user_section'); }); + after(async () => { + // login user back so we can clean up the user data for the next run + await driver.pause(3_000); + await waitForAppReady(); + }); + it('should start as anonymous', async () => { const statusEl = await scrollToEl('user_status_value'); const status = await statusEl.getText(); @@ -29,6 +35,7 @@ describe('User', () => { const externalId = await externalIdEl.getText(); expect(externalId).toBe(getTestExternalId()); + await driver.pause(3_000); await logoutUser(); statusEl = await scrollToEl('user_status_value'); diff --git a/appium/wdio.android.conf.ts b/appium/wdio.android.conf.ts index 4fdacb5..5a5a0d4 100644 --- a/appium/wdio.android.conf.ts +++ b/appium/wdio.android.conf.ts @@ -19,6 +19,9 @@ export const config: WebdriverIO.Config = { // Disable ID locator autocompletion to avoid Flutter's Semantics(container:true) wrapping inputs in a View. // @ts-expect-error - Appium types are not fully compatible with WebdriverIO types 'appium:settings[disableIdLocatorAutocompletion]': true, + + // Hide keyboard during session + 'appium:hideKeyboard': true, }, ], }; diff --git a/appium/wdio.ios.conf.ts b/appium/wdio.ios.conf.ts index 7056ebc..125e416 100644 --- a/appium/wdio.ios.conf.ts +++ b/appium/wdio.ios.conf.ts @@ -15,6 +15,9 @@ export const config: WebdriverIO.Config = { 'appium:autoAcceptAlerts': false, 'appium:noReset': true, ...(isLocal ? {} : { 'bstack:options': bstackOptions }), + + // Hide keyboard during session + 'appium:hideKeyboard': true, }, ], }; From 8acf5253b9b04530ee8efb4d1e62d9579e697d81 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 20:42:36 -0700 Subject: [PATCH 10/21] test(appium): enable chromedriver autodownload --- appium/scripts/run-local.sh | 2 +- appium/wdio.android.conf.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index 0ea9838..6c5931e 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -282,7 +282,7 @@ start_appium() { fi info "Starting Appium on port $APPIUM_PORT..." - appium --port "$APPIUM_PORT" --log-level error & + appium --port "$APPIUM_PORT" --log-level error --allow-insecure=uiautomator2:chromedriver_autodownload & local pid=$! local retries=0 diff --git a/appium/wdio.android.conf.ts b/appium/wdio.android.conf.ts index 5a5a0d4..5774c2b 100644 --- a/appium/wdio.android.conf.ts +++ b/appium/wdio.android.conf.ts @@ -22,6 +22,7 @@ export const config: WebdriverIO.Config = { // Hide keyboard during session 'appium:hideKeyboard': true, + 'appium:chromedriverAutodownload': true, }, ], }; From 5c801c4f2c70047273c7c30181bbd4b37378d509 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 22:51:51 -0700 Subject: [PATCH 11/21] test(appium): improve webview detection and IAM reliability --- appium/tests/helpers/app.ts | 103 ++++++++++++++++++++++++++---- appium/tests/helpers/selectors.ts | 7 ++ appium/tests/specs/03_iam.spec.ts | 10 ++- 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index 4dff8f2..681ab78 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -457,11 +457,45 @@ export async function checkNotification(opts: { export async function isWebViewVisible() { const platform = getPlatform(); - const webview = - platform === 'ios' - ? await $('-ios predicate string:type == "XCUIElementTypeWebView"') - : await $('android=new UiSelector().className("android.webkit.WebView")'); - return webview.isExisting(); + if (platform === 'ios') { + const webview = await $('-ios predicate string:type == "XCUIElementTypeWebView"'); + return await webview.isExisting(); + } + + const currentContext = await driver.getContext(); + let isVisible = false; + + try { + const contexts = await driver.getContexts(); + const webviewContexts = contexts.filter((c) => String(c).includes('WEBVIEW')); + + for (const context of webviewContexts) { + try { + await driver.switchContext(String(context)); + const url = await driver.getUrl(); + if (url && url !== 'about:blank' && url !== 'data:,') { + isVisible = true; + break; + } + } catch (err: any) { + if (err.message && err.message.includes('terminated')) { + continue; + } + } + } + } catch (e) { + // ignore + } finally { + try { + if (String(currentContext) === 'NATIVE_APP') { + await driver.switchContext('NATIVE_APP'); + } + } catch { + // ignore + } + } + + return isVisible; } export async function checkInAppMessage(opts: { @@ -482,10 +516,46 @@ export async function checkInAppMessage(opts: { }); await driver.pause(1_000); - const contexts = await driver.getContexts(); - const webviewContext = contexts.find((c) => String(c) !== 'NATIVE_APP'); - expect(webviewContext).toBeDefined(); - await driver.switchContext(String(webviewContext)); + let validContext: string | undefined; + + for (let i = 0; i < 15; i++) { + const contexts = await driver.getContexts(); + const webviewContexts = contexts.filter((c) => String(c).includes('WEBVIEW')).reverse(); + for (const c of webviewContexts) { + try { + await driver.switchContext(String(c)); + const handles = await driver.getWindowHandles(); + for (const handle of handles) { + await driver.switchToWindow(handle); + const titles = await $$('h1'); + if (titles.length > 0) { + for (const title of titles) { + const text = await title.getText(); + if (text === expectedTitle) { + validContext = String(c); + break; + } + } + if (validContext) break; + } + } + + // Always switch back to NATIVE_APP so the next getContexts/switchContext is fresh + await driver.switchContext('NATIVE_APP'); + } catch (err: any) { + if (err.message && err.message.includes('terminated')) { + continue; + } + } + } + if (validContext) break; + await driver.pause(1000); + } + + expect(validContext).toBeDefined(); + if (validContext) { + await driver.switchContext(validContext); + } const title = await $('h1'); await title.waitForExist({ timeout: timeoutMs }); @@ -496,10 +566,17 @@ export async function checkInAppMessage(opts: { await closeButton.click(); await driver.switchContext('NATIVE_APP'); - await driver.waitUntil(async () => !(await isWebViewVisible()), { - timeout: timeoutMs, - timeoutMsg: 'IAM webview still visible after closing', - }); + + if (getPlatform() === 'ios') { + await driver.waitUntil(async () => !(await isWebViewVisible()), { + timeout: timeoutMs, + timeoutMsg: 'IAM webview still visible after closing', + }); + } else { + // Appium Android caches the webview context heavily, so we can't reliably detect + // if the IAM webview is fully destroyed. We just wait for the animation. + await driver.pause(3000); + } } export async function checkTooltip(buttonId: string, key: string) { diff --git a/appium/tests/helpers/selectors.ts b/appium/tests/helpers/selectors.ts index 7e0039a..9c04d92 100644 --- a/appium/tests/helpers/selectors.ts +++ b/appium/tests/helpers/selectors.ts @@ -133,6 +133,13 @@ export async function deleteUser(externalId: string) { } } +export async function getToggleState(el: { getAttribute(name: string): Promise }): Promise { + if (getPlatform() === 'ios') { + return (await el.getAttribute('value')) === '1'; + } + return (await el.getAttribute('checked')) === 'true'; +} + export function getSdkType(): SdkType { const sdkType = process.env.SDK_TYPE; if (sdkType && VALID_SDK_TYPES.has(sdkType)) { diff --git a/appium/tests/specs/03_iam.spec.ts b/appium/tests/specs/03_iam.spec.ts index 498e54e..d1bdd60 100644 --- a/appium/tests/specs/03_iam.spec.ts +++ b/appium/tests/specs/03_iam.spec.ts @@ -5,6 +5,7 @@ import { scrollToEl, waitForAppReady, } from '../helpers/app'; +import { getToggleState } from '../helpers/selectors'; describe('In-App Messaging', () => { before(async () => { @@ -33,15 +34,18 @@ describe('In-App Messaging', () => { it('can pause iam', async () => { const toggle = await scrollToEl('Pause In-App', { by: 'text', partial: true, direction: 'up' }); - expect(await toggle.getAttribute('value')).toBe('0'); + expect(await getToggleState(toggle)).toBe(false); await toggle.click({ x: 0, y: 0 }); - expect(await toggle.getAttribute('value')).toBe('1'); + expect(await getToggleState(toggle)).toBe(true); // try to show top banner, should fail since IAM is paused const button = await scrollToEl('TOP BANNER', { by: 'text' }); await button.click(); await driver.pause(3_000); - expect(await isWebViewVisible()).toBe(false); + + if (driver.isIOS) { + expect(await isWebViewVisible()).toBe(false); + } // reset back await toggle.click({ x: 0, y: 0 }); From 2cbb1c680ab36c89400817c7bac03dd50de3dc93 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 23:09:13 -0700 Subject: [PATCH 12/21] test(appium): disable chromedriver autodownload --- appium/scripts/run-local.sh | 2 +- appium/tests/helpers/app.ts | 114 +++++++++++++----------------------- appium/wdio.android.conf.ts | 1 - 3 files changed, 41 insertions(+), 76 deletions(-) diff --git a/appium/scripts/run-local.sh b/appium/scripts/run-local.sh index 6c5931e..0ea9838 100755 --- a/appium/scripts/run-local.sh +++ b/appium/scripts/run-local.sh @@ -282,7 +282,7 @@ start_appium() { fi info "Starting Appium on port $APPIUM_PORT..." - appium --port "$APPIUM_PORT" --log-level error --allow-insecure=uiautomator2:chromedriver_autodownload & + appium --port "$APPIUM_PORT" --log-level error & local pid=$! local retries=0 diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index 681ab78..6a2dfe2 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -456,46 +456,54 @@ export async function checkNotification(opts: { } export async function isWebViewVisible() { - const platform = getPlatform(); - if (platform === 'ios') { + if (getPlatform() === 'ios') { const webview = await $('-ios predicate string:type == "XCUIElementTypeWebView"'); return await webview.isExisting(); } - const currentContext = await driver.getContext(); - let isVisible = false; + const contexts = await driver.getContexts(); + return contexts.some((c) => String(c).includes('WEBVIEW')); +} - try { - const contexts = await driver.getContexts(); - const webviewContexts = contexts.filter((c) => String(c).includes('WEBVIEW')); +/** + * On Android, Appium pools all IAM webviews under a single WEBVIEW_* context, + * so closing one IAM doesn't remove the context -- old window handles linger. + * We iterate window handles to find the one whose

matches the expected title. + */ +async function switchToWebViewContext() { + const contexts = await driver.getContexts(); + const webviewContext = contexts.find((c) => String(c) !== 'NATIVE_APP'); + if (!webviewContext) return false; + await driver.switchContext(String(webviewContext)); + return true; +} - for (const context of webviewContexts) { +async function switchToIAMWebView(expectedTitle: string, timeoutMs: number) { + if (getPlatform() === 'ios') { + expect(await switchToWebViewContext()).toBe(true); + return; + } + + await driver.waitUntil( + async () => { try { - await driver.switchContext(String(context)); - const url = await driver.getUrl(); - if (url && url !== 'about:blank' && url !== 'data:,') { - isVisible = true; - break; - } - } catch (err: any) { - if (err.message && err.message.includes('terminated')) { - continue; + if (!(await switchToWebViewContext())) return false; + + for (const handle of await driver.getWindowHandles()) { + await driver.switchToWindow(handle); + const h1 = await $('h1'); + if ((await h1.isExisting()) && (await h1.getText()) === expectedTitle) { + return true; + } } - } - } - } catch (e) { - // ignore - } finally { - try { - if (String(currentContext) === 'NATIVE_APP') { await driver.switchContext('NATIVE_APP'); + return false; + } catch { + return false; } - } catch { - // ignore - } - } - - return isVisible; + }, + { timeout: timeoutMs, timeoutMsg: `Could not find IAM with title "${expectedTitle}"` }, + ); } export async function checkInAppMessage(opts: { @@ -516,51 +524,11 @@ export async function checkInAppMessage(opts: { }); await driver.pause(1_000); - let validContext: string | undefined; - - for (let i = 0; i < 15; i++) { - const contexts = await driver.getContexts(); - const webviewContexts = contexts.filter((c) => String(c).includes('WEBVIEW')).reverse(); - for (const c of webviewContexts) { - try { - await driver.switchContext(String(c)); - const handles = await driver.getWindowHandles(); - for (const handle of handles) { - await driver.switchToWindow(handle); - const titles = await $$('h1'); - if (titles.length > 0) { - for (const title of titles) { - const text = await title.getText(); - if (text === expectedTitle) { - validContext = String(c); - break; - } - } - if (validContext) break; - } - } - - // Always switch back to NATIVE_APP so the next getContexts/switchContext is fresh - await driver.switchContext('NATIVE_APP'); - } catch (err: any) { - if (err.message && err.message.includes('terminated')) { - continue; - } - } - } - if (validContext) break; - await driver.pause(1000); - } - - expect(validContext).toBeDefined(); - if (validContext) { - await driver.switchContext(validContext); - } + await switchToIAMWebView(expectedTitle, timeoutMs); const title = await $('h1'); await title.waitForExist({ timeout: timeoutMs }); - const text = await title.getText(); - expect(text).toBe(expectedTitle); + expect(await title.getText()).toBe(expectedTitle); const closeButton = await $('.close-button'); await closeButton.click(); @@ -573,8 +541,6 @@ export async function checkInAppMessage(opts: { timeoutMsg: 'IAM webview still visible after closing', }); } else { - // Appium Android caches the webview context heavily, so we can't reliably detect - // if the IAM webview is fully destroyed. We just wait for the animation. await driver.pause(3000); } } diff --git a/appium/wdio.android.conf.ts b/appium/wdio.android.conf.ts index 5774c2b..5a5a0d4 100644 --- a/appium/wdio.android.conf.ts +++ b/appium/wdio.android.conf.ts @@ -22,7 +22,6 @@ export const config: WebdriverIO.Config = { // Hide keyboard during session 'appium:hideKeyboard': true, - 'appium:chromedriverAutodownload': true, }, ], }; From 7d62ddb6fd3825e43f03d563425f5086ef4f1409 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 23:17:55 -0700 Subject: [PATCH 13/21] test(appium): extract text input helper for Flutter --- appium/tests/helpers/app.ts | 37 +++++++++++++++-------------- appium/tests/specs/04_alias.spec.ts | 14 +++++------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index 6a2dfe2..6784da5 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -217,24 +217,9 @@ export async function loginUser(externalUserId: string) { const loginButton = await byText('LOGIN USER'); await loginButton.click(); - if (getPlatform() === 'android' && getSdkType() === 'flutter') { - const userIdInput = await byTestId('login_user_id_input'); - await userIdInput.waitForDisplayed({ timeout: 5_000 }); - await userIdInput.click(); - await driver.pause(250); - await driver.execute('mobile: type', { text: externalUserId }); - const confirmButton = await byText('Login'); - await browser.waitUntil(async () => confirmButton.isEnabled(), { - timeout: 5_000, - timeoutMsg: 'Expected Login button to enable', - }); - await confirmButton.click(); - return; - } - const userIdInput = await byTestId('login_user_id_input'); await userIdInput.waitForDisplayed({ timeout: 5_000 }); - await userIdInput.setValue(externalUserId); + await typeIntoInput(userIdInput, externalUserId); const confirmButton = await byTestId('login_confirm_button'); await confirmButton.click(); @@ -256,6 +241,22 @@ export async function togglePushEnabled() { await toggle.click(); } +/** + * Type text into an input field. On Flutter Android, setValue is unreliable + * so we tap the field and use the native `mobile: type` command instead. + */ +export async function typeIntoInput( + el: { click(): Promise; setValue(value: string): Promise }, + text: string, +) { + if (getPlatform() === 'android' && getSdkType() === 'flutter') { + await el.click(); + await driver.execute('mobile: type', { text }); + } else { + await el.setValue(text); + } +} + /** * Add a single tag via the UI. */ @@ -265,10 +266,10 @@ export async function addTag(key: string, value: string) { const keyInput = await byTestId('tag_key_input'); await keyInput.waitForDisplayed({ timeout: 5_000 }); - await keyInput.setValue(key); + await typeIntoInput(keyInput, key); const valueInput = await byTestId('tag_value_input'); - await valueInput.setValue(value); + await typeIntoInput(valueInput, value); const confirmButton = await byTestId('tag_confirm_button'); await confirmButton.click(); diff --git a/appium/tests/specs/04_alias.spec.ts b/appium/tests/specs/04_alias.spec.ts index aa656c8..6d7fe4e 100644 --- a/appium/tests/specs/04_alias.spec.ts +++ b/appium/tests/specs/04_alias.spec.ts @@ -1,4 +1,4 @@ -import { checkTooltip, expectPairInSection, scrollToEl, waitForAppReady } from '../helpers/app'; +import { checkTooltip, expectPairInSection, scrollToEl, typeIntoInput, waitForAppReady } from '../helpers/app'; import { byTestId, byText } from '../helpers/selectors.js'; describe('Aliases', () => { @@ -17,10 +17,10 @@ describe('Aliases', () => { const labelInput = await byTestId('alias_label_input'); await labelInput.waitForDisplayed({ timeout: 5_000 }); - await labelInput.setValue('test_label'); + await typeIntoInput(labelInput, 'test_label'); const idInput = await byTestId('alias_id_input'); - await idInput.setValue('test_id'); + await typeIntoInput(idInput, 'test_id'); const confirmButton = await byText('Add'); await confirmButton.click(); @@ -37,19 +37,19 @@ describe('Aliases', () => { const label0 = await byTestId('Label_input_0'); await label0.waitForDisplayed({ timeout: 5_000 }); - await label0.setValue('test_label_2'); + await typeIntoInput(label0, 'test_label_2'); const id0 = await byTestId('ID_input_0'); await id0.waitForDisplayed({ timeout: 5_000 }); - await id0.setValue('test_id_2'); + await typeIntoInput(id0, 'test_id_2'); const label1 = await byTestId('Label_input_1'); await label1.waitForDisplayed({ timeout: 5_000 }); - await label1.setValue('test_label_3'); + await typeIntoInput(label1, 'test_label_3'); const id1 = await byTestId('ID_input_1'); await id1.waitForDisplayed({ timeout: 5_000 }); - await id1.setValue('test_id_3'); + await typeIntoInput(id1, 'test_id_3'); const confirmButton = await byText('Add All'); await confirmButton.click(); From 2dbbac765f01d269c3258acea983aa1ef0affc18 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 23:24:48 -0700 Subject: [PATCH 14/21] refactor(appium): rename typeIntoInput to typeInto --- appium/tests/helpers/app.ts | 8 ++++---- appium/tests/specs/04_alias.spec.ts | 14 +++++++------- appium/tests/specs/05_email.spec.ts | 4 ++-- appium/tests/specs/06_sms.spec.ts | 4 ++-- appium/tests/specs/07_tag.spec.ts | 14 +++++++------- appium/tests/specs/08_outcome.spec.ts | 10 +++++----- appium/tests/specs/09_trigger.spec.ts | 14 +++++++------- appium/tests/specs/10_event.spec.ts | 9 ++++----- 8 files changed, 38 insertions(+), 39 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index 6784da5..35f3215 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -219,7 +219,7 @@ export async function loginUser(externalUserId: string) { const userIdInput = await byTestId('login_user_id_input'); await userIdInput.waitForDisplayed({ timeout: 5_000 }); - await typeIntoInput(userIdInput, externalUserId); + await typeInto(userIdInput, externalUserId); const confirmButton = await byTestId('login_confirm_button'); await confirmButton.click(); @@ -245,7 +245,7 @@ export async function togglePushEnabled() { * Type text into an input field. On Flutter Android, setValue is unreliable * so we tap the field and use the native `mobile: type` command instead. */ -export async function typeIntoInput( +export async function typeInto( el: { click(): Promise; setValue(value: string): Promise }, text: string, ) { @@ -266,10 +266,10 @@ export async function addTag(key: string, value: string) { const keyInput = await byTestId('tag_key_input'); await keyInput.waitForDisplayed({ timeout: 5_000 }); - await typeIntoInput(keyInput, key); + await typeInto(keyInput, key); const valueInput = await byTestId('tag_value_input'); - await typeIntoInput(valueInput, value); + await typeInto(valueInput, value); const confirmButton = await byTestId('tag_confirm_button'); await confirmButton.click(); diff --git a/appium/tests/specs/04_alias.spec.ts b/appium/tests/specs/04_alias.spec.ts index 6d7fe4e..aca310a 100644 --- a/appium/tests/specs/04_alias.spec.ts +++ b/appium/tests/specs/04_alias.spec.ts @@ -1,4 +1,4 @@ -import { checkTooltip, expectPairInSection, scrollToEl, typeIntoInput, waitForAppReady } from '../helpers/app'; +import { checkTooltip, expectPairInSection, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; import { byTestId, byText } from '../helpers/selectors.js'; describe('Aliases', () => { @@ -17,10 +17,10 @@ describe('Aliases', () => { const labelInput = await byTestId('alias_label_input'); await labelInput.waitForDisplayed({ timeout: 5_000 }); - await typeIntoInput(labelInput, 'test_label'); + await typeInto(labelInput, 'test_label'); const idInput = await byTestId('alias_id_input'); - await typeIntoInput(idInput, 'test_id'); + await typeInto(idInput, 'test_id'); const confirmButton = await byText('Add'); await confirmButton.click(); @@ -37,19 +37,19 @@ describe('Aliases', () => { const label0 = await byTestId('Label_input_0'); await label0.waitForDisplayed({ timeout: 5_000 }); - await typeIntoInput(label0, 'test_label_2'); + await typeInto(label0, 'test_label_2'); const id0 = await byTestId('ID_input_0'); await id0.waitForDisplayed({ timeout: 5_000 }); - await typeIntoInput(id0, 'test_id_2'); + await typeInto(id0, 'test_id_2'); const label1 = await byTestId('Label_input_1'); await label1.waitForDisplayed({ timeout: 5_000 }); - await typeIntoInput(label1, 'test_label_3'); + await typeInto(label1, 'test_label_3'); const id1 = await byTestId('ID_input_1'); await id1.waitForDisplayed({ timeout: 5_000 }); - await typeIntoInput(id1, 'test_id_3'); + await typeInto(id1, 'test_id_3'); const confirmButton = await byText('Add All'); await confirmButton.click(); diff --git a/appium/tests/specs/05_email.spec.ts b/appium/tests/specs/05_email.spec.ts index 75499ea..1455776 100644 --- a/appium/tests/specs/05_email.spec.ts +++ b/appium/tests/specs/05_email.spec.ts @@ -1,4 +1,4 @@ -import { checkTooltip, scrollToEl, waitForAppReady } from '../helpers/app'; +import { checkTooltip, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; import { byTestId, byText, getTestData } from '../helpers/selectors.js'; describe('Emails', () => { @@ -20,7 +20,7 @@ describe('Emails', () => { const emailInput = await byTestId('Email_input'); await emailInput.waitForDisplayed({ timeout: 5_000 }); - await emailInput.setValue(email); + await typeInto(emailInput, email); const confirmButton = await byText('Add'); await confirmButton.click(); diff --git a/appium/tests/specs/06_sms.spec.ts b/appium/tests/specs/06_sms.spec.ts index ac305b0..e8a0314 100644 --- a/appium/tests/specs/06_sms.spec.ts +++ b/appium/tests/specs/06_sms.spec.ts @@ -1,4 +1,4 @@ -import { checkTooltip, scrollToEl, waitForAppReady } from '../helpers/app'; +import { checkTooltip, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; import { byTestId, byText, getTestData } from '../helpers/selectors.js'; describe('SMS', () => { @@ -19,7 +19,7 @@ describe('SMS', () => { const smsInput = await byTestId('SMS Number_input'); await smsInput.waitForDisplayed({ timeout: 5_000 }); - await smsInput.setValue(sms); + await typeInto(smsInput, sms); const confirmButton = await byText('Add'); await confirmButton.click(); diff --git a/appium/tests/specs/07_tag.spec.ts b/appium/tests/specs/07_tag.spec.ts index e129060..5b60314 100644 --- a/appium/tests/specs/07_tag.spec.ts +++ b/appium/tests/specs/07_tag.spec.ts @@ -1,4 +1,4 @@ -import { checkTooltip, expectPairInSection, scrollToEl, waitForAppReady } from '../helpers/app'; +import { checkTooltip, expectPairInSection, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; import { byTestId, byText } from '../helpers/selectors.js'; describe('Tags', () => { @@ -18,10 +18,10 @@ describe('Tags', () => { // add tag const keyInput = await byTestId('tag_key_input'); await keyInput.waitForDisplayed({ timeout: 5_000 }); - await keyInput.setValue('test_tag'); + await typeInto(keyInput, 'test_tag'); const valueInput = await byTestId('tag_value_input'); - await valueInput.setValue('test_tag_value'); + await typeInto(valueInput, 'test_tag_value'); const confirmButton = await byTestId('tag_confirm_button'); await confirmButton.click(); @@ -43,20 +43,20 @@ describe('Tags', () => { // add tags const key0 = await byTestId('Key_input_0'); await key0.waitForDisplayed({ timeout: 5_000 }); - await key0.setValue('test_tag_2'); + await typeInto(key0, 'test_tag_2'); const value0 = await byTestId('Value_input_0'); - await value0.setValue('test_tag_value_2'); + await typeInto(value0, 'test_tag_value_2'); const addRowButton = await byText('Add Row'); await addRowButton.click(); const key1 = await byTestId('Key_input_1'); await key1.waitForDisplayed({ timeout: 5_000 }); - await key1.setValue('test_tag_3'); + await typeInto(key1, 'test_tag_3'); const value1 = await byTestId('Value_input_1'); - await value1.setValue('test_tag_value_3'); + await typeInto(value1, 'test_tag_value_3'); let confirmButton = await byText('Add All'); await confirmButton.click(); diff --git a/appium/tests/specs/08_outcome.spec.ts b/appium/tests/specs/08_outcome.spec.ts index cfdcf18..57b5f7f 100644 --- a/appium/tests/specs/08_outcome.spec.ts +++ b/appium/tests/specs/08_outcome.spec.ts @@ -1,4 +1,4 @@ -import { checkTooltip, scrollToEl, waitForAppReady } from '../helpers/app'; +import { checkTooltip, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; import { byTestId, byText } from '../helpers/selectors.js'; describe('Outcomes', () => { @@ -17,7 +17,7 @@ describe('Outcomes', () => { const nameInput = await byTestId('outcome_name_input'); await nameInput.waitForDisplayed({ timeout: 5_000 }); - await nameInput.setValue('test_normal'); + await typeInto(nameInput, 'test_normal'); const normalRadio = await byText('Normal Outcome'); await normalRadio.click(); @@ -35,7 +35,7 @@ describe('Outcomes', () => { const nameInput = await byTestId('outcome_name_input'); await nameInput.waitForDisplayed({ timeout: 5_000 }); - await nameInput.setValue('test_unique'); + await typeInto(nameInput, 'test_unique'); const uniqueRadio = await byText('Unique Outcome'); await uniqueRadio.click(); @@ -57,11 +57,11 @@ describe('Outcomes', () => { const withValueRadio = await byText('Outcome with Value'); await withValueRadio.click(); - await nameInput.setValue('test_valued'); + await typeInto(nameInput, 'test_valued'); const valueInput = await byTestId('outcome_value_input'); await valueInput.waitForDisplayed({ timeout: 5_000 }); - await valueInput.setValue('3.14'); + await typeInto(valueInput, '3.14'); const sendBtn = await byTestId('outcome_send_button'); await sendBtn.click(); diff --git a/appium/tests/specs/09_trigger.spec.ts b/appium/tests/specs/09_trigger.spec.ts index f369d48..fa06062 100644 --- a/appium/tests/specs/09_trigger.spec.ts +++ b/appium/tests/specs/09_trigger.spec.ts @@ -1,4 +1,4 @@ -import { checkTooltip, expectPairInSection, scrollToEl, waitForAppReady } from '../helpers/app'; +import { checkTooltip, expectPairInSection, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; import { byTestId, byText } from '../helpers/selectors.js'; async function addMultipleTriggers() { @@ -10,16 +10,16 @@ async function addMultipleTriggers() { const key0 = await byTestId('Key_input_0'); await key0.waitForDisplayed({ timeout: 5_000 }); - await key0.setValue('test_trigger_key_2'); + await typeInto(key0, 'test_trigger_key_2'); const value0 = await byTestId('Value_input_0'); - await value0.setValue('test_trigger_value_2'); + await typeInto(value0, 'test_trigger_value_2'); const key1 = await byTestId('Key_input_1'); - await key1.setValue('test_trigger_key_3'); + await typeInto(key1, 'test_trigger_key_3'); const value1 = await byTestId('Value_input_1'); - await value1.setValue('test_trigger_value_3'); + await typeInto(value1, 'test_trigger_value_3'); let confirmButton = await byText('Add All'); await confirmButton.click(); @@ -45,10 +45,10 @@ describe('Triggers', () => { // add trigger const keyInput = await byTestId('trigger_key_input'); await keyInput.waitForDisplayed({ timeout: 5_000 }); - await keyInput.setValue('test_trigger_key'); + await typeInto(keyInput, 'test_trigger_key'); const valueInput = await byTestId('trigger_value_input'); - await valueInput.setValue('test_trigger_value'); + await typeInto(valueInput, 'test_trigger_value'); const confirmButton = await byTestId('trigger_confirm_button'); await confirmButton.click(); diff --git a/appium/tests/specs/10_event.spec.ts b/appium/tests/specs/10_event.spec.ts index 816d169..085369d 100644 --- a/appium/tests/specs/10_event.spec.ts +++ b/appium/tests/specs/10_event.spec.ts @@ -1,4 +1,4 @@ -import { scrollToEl, waitForAppReady } from '../helpers/app'; +import { scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; import { byTestId, byText, getTestData } from '../helpers/selectors.js'; const TEST_JSON = { @@ -36,7 +36,7 @@ describe('Custom Events', () => { const nameInput = await byTestId('event_name_input'); await nameInput.waitForDisplayed({ timeout: 5_000 }); - await nameInput.setValue(`${customEvent}_no_props`); + await typeInto(nameInput, `${customEvent}_no_props`); const trackBtn = await byTestId('event_track_button'); await trackBtn.click(); @@ -52,11 +52,10 @@ describe('Custom Events', () => { const nameInput = await byTestId('event_name_input'); await nameInput.waitForDisplayed({ timeout: 5_000 }); - await nameInput.setValue(`${customEvent}_with_props`); + await typeInto(nameInput, `${customEvent}_with_props`); const propertiesInput = await byTestId('event_properties_input'); - await propertiesInput.click(); - await propertiesInput.setValue(JSON.stringify(TEST_JSON)); + await typeInto(propertiesInput, JSON.stringify(TEST_JSON)); const trackBtn = await byTestId('event_track_button'); await trackBtn.click(); From 26170b8f900c7026444429d2fdbbfeed43824f7d Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 23:29:47 -0700 Subject: [PATCH 15/21] test(appium): fix typeInto logic and scrollToEl call --- appium/tests/helpers/app.ts | 8 ++++---- appium/tests/specs/07_tag.spec.ts | 10 ++++++++-- appium/tests/specs/09_trigger.spec.ts | 12 +++++++++--- appium/tests/specs/debug.spec.ts | 2 +- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index 35f3215..e5ca9a0 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -95,7 +95,7 @@ export async function scrollToEl( maxScrolls?: number; } = {}, ) { - const { by = 'testId', partial = false, direction = 'down', maxScrolls = 10 } = opts; + const { by = 'testId', partial = false, direction = 'down', maxScrolls = 20 } = opts; const finder = (id: string) => (by === 'text' ? byText(id, partial) : byTestId(id)); for (let i = 0; i < maxScrolls; i++) { @@ -103,7 +103,7 @@ export async function scrollToEl( if (await el.isDisplayed()) { return el; } - await swipeMainContent(direction, 'small'); + await swipeMainContent(direction); } throw new Error(`Element "${identifier}" not found after ${maxScrolls} scrolls`); } @@ -252,9 +252,9 @@ export async function typeInto( if (getPlatform() === 'android' && getSdkType() === 'flutter') { await el.click(); await driver.execute('mobile: type', { text }); - } else { - await el.setValue(text); + return; } + await el.setValue(text); } /** diff --git a/appium/tests/specs/07_tag.spec.ts b/appium/tests/specs/07_tag.spec.ts index 5b60314..cadf5ff 100644 --- a/appium/tests/specs/07_tag.spec.ts +++ b/appium/tests/specs/07_tag.spec.ts @@ -1,4 +1,10 @@ -import { checkTooltip, expectPairInSection, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; +import { + checkTooltip, + expectPairInSection, + scrollToEl, + typeInto, + waitForAppReady, +} from '../helpers/app'; import { byTestId, byText } from '../helpers/selectors.js'; describe('Tags', () => { @@ -65,7 +71,7 @@ describe('Tags', () => { await expectPairInSection('tags', 'test_tag_3', 'test_tag_value_3'); // remove tags - const removeButton = await scrollToEl('REMOVE TAGS'); + const removeButton = await scrollToEl('REMOVE TAGS', { by: 'text' }); await removeButton.click(); const tag2Checkbox = await byTestId('remove_checkbox_test_tag_2'); diff --git a/appium/tests/specs/09_trigger.spec.ts b/appium/tests/specs/09_trigger.spec.ts index fa06062..44ae0dd 100644 --- a/appium/tests/specs/09_trigger.spec.ts +++ b/appium/tests/specs/09_trigger.spec.ts @@ -1,4 +1,10 @@ -import { checkTooltip, expectPairInSection, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; +import { + checkTooltip, + expectPairInSection, + scrollToEl, + typeInto, + waitForAppReady, +} from '../helpers/app'; import { byTestId, byText } from '../helpers/selectors.js'; async function addMultipleTriggers() { @@ -67,7 +73,7 @@ describe('Triggers', () => { await addMultipleTriggers(); // remove triggers - const removeButton = await scrollToEl('REMOVE TRIGGERS'); + const removeButton = await scrollToEl('REMOVE TRIGGERS', { by: 'text' }); await removeButton.click(); const trigger2Checkbox = await byTestId('remove_checkbox_test_trigger_key_2'); @@ -93,7 +99,7 @@ describe('Triggers', () => { await addMultipleTriggers(); // clear all triggers - const clearButton = await scrollToEl('CLEAR ALL TRIGGERS'); + const clearButton = await scrollToEl('CLEAR ALL TRIGGERS', { by: 'text' }); await clearButton.click(); await scrollToEl('triggers_section', { direction: 'up' }); diff --git a/appium/tests/specs/debug.spec.ts b/appium/tests/specs/debug.spec.ts index 7dd3702..3973ad7 100644 --- a/appium/tests/specs/debug.spec.ts +++ b/appium/tests/specs/debug.spec.ts @@ -1,4 +1,4 @@ -describe('Debug', () => { +describe.skip('Debug', () => { it('log ui hierarchy', async () => { const html = await driver.getPageSource(); console.log(html); From 496d4b4ef13bb78bc30bedbe7c2f96b6e1d221ea Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 23:50:56 -0700 Subject: [PATCH 16/21] test(appium): fix element positioning and Android alerts --- appium/tests/helpers/app.ts | 8 +++++++- appium/tests/specs/11_location.spec.ts | 20 +++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index e5ca9a0..be157c3 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -99,8 +99,14 @@ export async function scrollToEl( const finder = (id: string) => (by === 'text' ? byText(id, partial) : byTestId(id)); for (let i = 0; i < maxScrolls; i++) { - const el = await finder(identifier); + let el = await finder(identifier); if (await el.isDisplayed()) { + const { y } = await el.getLocation(); + const { height } = await driver.getWindowSize(); + if (y > height * 0.9) { + await swipeMainContent(direction, 'small'); + el = await finder(identifier); + } return el; } await swipeMainContent(direction); diff --git a/appium/tests/specs/11_location.spec.ts b/appium/tests/specs/11_location.spec.ts index 9b00a98..adbca4b 100644 --- a/appium/tests/specs/11_location.spec.ts +++ b/appium/tests/specs/11_location.spec.ts @@ -1,5 +1,5 @@ import { waitForAppReady, waitForAlert, scrollToEl, checkTooltip } from '../helpers/app.js'; -import { byText } from '../helpers/selectors.js'; +import { byText, getPlatform } from '../helpers/selectors.js'; describe('Location', () => { before(async () => { @@ -16,13 +16,19 @@ describe('Location', () => { 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', - }); + if (getPlatform() === 'ios') { + const alert = await waitForAlert(); + expect(alert).toContain('location'); + await driver.execute('mobile: alert', { + action: 'accept', + buttonLabel: 'Allow While Using App', + }); + } else { + const allowBtn = await byText('While using the app'); + await allowBtn.waitForDisplayed({ timeout: 10_000 }); + await allowBtn.click(); + } }); it('can share location', async () => { From 6d33998a9689750179632c3f9eadd563337f35c7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 23:52:38 -0700 Subject: [PATCH 17/21] test(appium): fix iOS check order in activity tests --- appium/tests/specs/12_activity.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/tests/specs/12_activity.spec.ts b/appium/tests/specs/12_activity.spec.ts index 7fe161d..125d7ca 100644 --- a/appium/tests/specs/12_activity.spec.ts +++ b/appium/tests/specs/12_activity.spec.ts @@ -26,10 +26,10 @@ async function checkActivity(options: { orderId?: string; status: string; messag describe('Live Activities', () => { before(async function () { + await waitForAppReady({ skipLogin: true }); if (getPlatform() !== 'ios') { return this.skip(); } - await waitForAppReady({ skipLogin: true }); await scrollToEl('live_activities_section'); }); From 1ba516d4c2bd446558550a9167b8c8fbc07e4781 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 15 Apr 2026 10:52:42 -0700 Subject: [PATCH 18/21] test(appium): simplify typeInto and update event tests --- appium/tests/helpers/app.ts | 6 +----- appium/tests/specs/10_event.spec.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index be157c3..447b1ff 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -255,11 +255,7 @@ export async function typeInto( el: { click(): Promise; setValue(value: string): Promise }, text: string, ) { - if (getPlatform() === 'android' && getSdkType() === 'flutter') { - await el.click(); - await driver.execute('mobile: type', { text }); - return; - } + await el.click(); await el.setValue(text); } diff --git a/appium/tests/specs/10_event.spec.ts b/appium/tests/specs/10_event.spec.ts index 085369d..7cf216d 100644 --- a/appium/tests/specs/10_event.spec.ts +++ b/appium/tests/specs/10_event.spec.ts @@ -1,4 +1,4 @@ -import { scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; +import { checkTooltip, scrollToEl, typeInto, waitForAppReady } from '../helpers/app'; import { byTestId, byText, getTestData } from '../helpers/selectors.js'; const TEST_JSON = { @@ -25,9 +25,9 @@ describe('Custom Events', () => { }); // wait for rename when merged to main - // it('should show correct tooltip info', async () => { - // await checkTooltip('custom_events_info_icon', 'trackEvent'); - // }); + it('should show correct tooltip info', async () => { + await checkTooltip('custom_events_info_icon', 'customEvents'); + }); it('can send a custom event with no properties', async () => { const { customEvent } = getTestData(); @@ -38,7 +38,7 @@ describe('Custom Events', () => { await nameInput.waitForDisplayed({ timeout: 5_000 }); await typeInto(nameInput, `${customEvent}_no_props`); - const trackBtn = await byTestId('event_track_button'); + const trackBtn = await byText('Track'); await trackBtn.click(); const snackbar = await byText(`Event tracked: ${customEvent}_no_props`); @@ -55,9 +55,10 @@ describe('Custom Events', () => { await typeInto(nameInput, `${customEvent}_with_props`); const propertiesInput = await byTestId('event_properties_input'); - await typeInto(propertiesInput, JSON.stringify(TEST_JSON)); + const json = JSON.stringify(TEST_JSON); + await typeInto(propertiesInput, json); - const trackBtn = await byTestId('event_track_button'); + const trackBtn = await byText('Track'); await trackBtn.click(); const snackbar = await byText(`Event tracked: ${customEvent}_with_props`); From 782bc25477a874f36e78158de70b2d78db7865f8 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 15 Apr 2026 11:32:38 -0700 Subject: [PATCH 19/21] test(appium): clean up IAM test formatting and logging --- appium/tests/helpers/app.ts | 3 ++- appium/tests/helpers/selectors.ts | 17 +++++++++-------- appium/tests/specs/03_iam.spec.ts | 6 +++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/appium/tests/helpers/app.ts b/appium/tests/helpers/app.ts index 447b1ff..e8422a0 100644 --- a/appium/tests/helpers/app.ts +++ b/appium/tests/helpers/app.ts @@ -543,8 +543,9 @@ export async function checkInAppMessage(opts: { timeout: timeoutMs, timeoutMsg: 'IAM webview still visible after closing', }); + await driver.pause(1_000); } else { - await driver.pause(3000); + await driver.pause(3_000); } } diff --git a/appium/tests/helpers/selectors.ts b/appium/tests/helpers/selectors.ts index 9c04d92..f145964 100644 --- a/appium/tests/helpers/selectors.ts +++ b/appium/tests/helpers/selectors.ts @@ -112,7 +112,7 @@ export function getTestData() { } export async function deleteUser(externalId: string) { - console.log(`Deleting user: ${externalId}`); + console.info(`Deleting user: ${externalId}`); try { const response = await fetch( `https://api.onesignal.com/apps/${process.env.ONESIGNAL_APP_ID}/users/by/external_id/${externalId}`, @@ -127,13 +127,15 @@ export async function deleteUser(externalId: string) { if (!response.ok) { throw new Error(`Failed to delete user: ${response.statusText}`); } - console.log(`User deleted successfully`); + console.info(`User deleted successfully`); } catch (error) { console.error(`Failed to delete user: ${error}`); } } -export async function getToggleState(el: { getAttribute(name: string): Promise }): Promise { +export async function getToggleState(el: { + getAttribute(name: string): Promise; +}): Promise { if (getPlatform() === 'ios') { return (await el.getAttribute('value')) === '1'; } @@ -172,8 +174,9 @@ function withFlutterAndroidFixes }>(el: T for (const attr of attrs) { try { const val = ( - await (target as unknown as { getAttribute(n: string): Promise }) - .getAttribute(attr) + await ( + target as unknown as { getAttribute(n: string): Promise } + ).getAttribute(attr) )?.trim(); if (val) return val; } catch { @@ -233,7 +236,5 @@ export async function byText(text: string, partial = false) { ); } - return withFlutterAndroidFixes( - await $(`//*[@content-desc="${text}" or @text="${text}"]`), - ); + return withFlutterAndroidFixes(await $(`//*[@content-desc="${text}" or @text="${text}"]`)); } diff --git a/appium/tests/specs/03_iam.spec.ts b/appium/tests/specs/03_iam.spec.ts index d1bdd60..57f8249 100644 --- a/appium/tests/specs/03_iam.spec.ts +++ b/appium/tests/specs/03_iam.spec.ts @@ -35,20 +35,20 @@ describe('In-App Messaging', () => { const toggle = await scrollToEl('Pause In-App', { by: 'text', partial: true, direction: 'up' }); expect(await getToggleState(toggle)).toBe(false); - await toggle.click({ x: 0, y: 0 }); + await toggle.click(); expect(await getToggleState(toggle)).toBe(true); // try to show top banner, should fail since IAM is paused const button = await scrollToEl('TOP BANNER', { by: 'text' }); await button.click(); await driver.pause(3_000); - + if (driver.isIOS) { expect(await isWebViewVisible()).toBe(false); } // reset back - await toggle.click({ x: 0, y: 0 }); + await toggle.click(); await checkInAppMessage({ buttonLabel: 'TOP BANNER', expectedTitle: 'Top Banner', From 7410cc73816ca10e5a8f7d01d28e46be5636cf67 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 15 Apr 2026 11:36:12 -0700 Subject: [PATCH 20/21] test(appium): remove .only from image notification test --- appium/tests/specs/02_push.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/tests/specs/02_push.spec.ts b/appium/tests/specs/02_push.spec.ts index 524657e..28ea7ef 100644 --- a/appium/tests/specs/02_push.spec.ts +++ b/appium/tests/specs/02_push.spec.ts @@ -22,7 +22,7 @@ describe('Push Subscription', () => { expect(value).toBe('1'); }); - it.only('can send an image notification', async () => { + it('can send an image notification', async () => { await checkNotification({ buttonId: 'send_image_button', title: 'Image Notification', From a6fff0bb4e2b0c8cdc91451fc232d58ec20492dc Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 15 Apr 2026 11:46:07 -0700 Subject: [PATCH 21/21] test(appium): remove debug test file --- appium/tests/specs/debug.spec.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 appium/tests/specs/debug.spec.ts diff --git a/appium/tests/specs/debug.spec.ts b/appium/tests/specs/debug.spec.ts deleted file mode 100644 index 3973ad7..0000000 --- a/appium/tests/specs/debug.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -describe.skip('Debug', () => { - it('log ui hierarchy', async () => { - const html = await driver.getPageSource(); - console.log(html); - }); - // it('can debug', async () => { - // await scrollToTop(); - // }); -});