Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
98e617f
test: improve system alert handling in Appium
fadi-george Apr 14, 2026
82f35f7
test: refactor system alert handling logic
fadi-george Apr 15, 2026
e2b97db
test(appium): move Flutter Android settings to config
fadi-george Apr 15, 2026
a0b49f0
test(appium): refactor notification clearing logic
fadi-george Apr 15, 2026
360022a
test(appium): fix Android scroll handling
fadi-george Apr 15, 2026
2359afc
test(appium): refactor returnToApp for cross-platform
fadi-george Apr 15, 2026
9557041
test(appium): remove text fallback proxy logic
fadi-george Apr 15, 2026
7bacc69
test(appium): add Flutter Android getText fixes
fadi-george Apr 15, 2026
0bc2771
test(appium): improve alert handling and stability
fadi-george Apr 15, 2026
8acf525
test(appium): enable chromedriver autodownload
fadi-george Apr 15, 2026
5c801c4
test(appium): improve webview detection and IAM reliability
fadi-george Apr 15, 2026
2cbb1c6
test(appium): disable chromedriver autodownload
fadi-george Apr 15, 2026
7d62ddb
test(appium): extract text input helper for Flutter
fadi-george Apr 15, 2026
2dbbac7
refactor(appium): rename typeIntoInput to typeInto
fadi-george Apr 15, 2026
26170b8
test(appium): fix typeInto logic and scrollToEl call
fadi-george Apr 15, 2026
496d4b4
test(appium): fix element positioning and Android alerts
fadi-george Apr 15, 2026
6d33998
test(appium): fix iOS check order in activity tests
fadi-george Apr 15, 2026
1ba516d
test(appium): simplify typeInto and update event tests
fadi-george Apr 15, 2026
782bc25
test(appium): clean up IAM test formatting and logging
fadi-george Apr 15, 2026
7410cc7
test(appium): remove .only from image notification test
fadi-george Apr 15, 2026
a6fff0b
test(appium): remove debug test file
fadi-george Apr 15, 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
258 changes: 177 additions & 81 deletions appium/tests/helpers/app.ts

Large diffs are not rendered by default.

100 changes: 76 additions & 24 deletions appium/tests/helpers/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand All @@ -127,12 +127,21 @@ 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<string | null>;
}): Promise<boolean> {
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)) {
Expand All @@ -143,28 +152,70 @@ 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<T extends { getText(): Promise<string> }>(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<string | null> }
).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.
*
* 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 $(`[data-testid="${id}"]`);
if (sdkType === 'flutter' && platform === 'android')
return withFlutterAndroidFixes(await $(`id=${id}`));

return $(`~${id}`);
}

/**
Expand All @@ -173,16 +224,17 @@ export async function byTestId(id: string) {
*/
export async function byText(text: string, partial = false) {
const platform = getPlatform();
const sdkType = getSdkType();

if (sdkType === 'capacitor') {
return $(`//*[contains(text(), "${text}")]`);
}

if (platform === 'ios') {
const op = partial ? 'CONTAINS' : '==';
return $(`-ios predicate string:label ${op} "${text}"`);
}
const method = partial ? 'textContains' : 'text';
return $(`android=new UiSelector().${method}("${text}")`);

if (partial) {
return withFlutterAndroidFixes(
await $(`//*[contains(@content-desc, "${text}") or contains(@text, "${text}")]`),
);
}

return withFlutterAndroidFixes(await $(`//*[@content-desc="${text}" or @text="${text}"]`));
}
7 changes: 7 additions & 0 deletions appium/tests/specs/01_user.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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');
Expand Down
14 changes: 9 additions & 5 deletions appium/tests/specs/03_iam.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
scrollToEl,
waitForAppReady,
} from '../helpers/app';
import { getToggleState } from '../helpers/selectors';

describe('In-App Messaging', () => {
before(async () => {
Expand Down Expand Up @@ -33,18 +34,21 @@ 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');
await toggle.click({ x: 0, y: 0 });
expect(await toggle.getAttribute('value')).toBe('1');
expect(await getToggleState(toggle)).toBe(false);
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);
expect(await isWebViewVisible()).toBe(false);

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',
Expand Down
14 changes: 7 additions & 7 deletions appium/tests/specs/04_alias.spec.ts
Original file line number Diff line number Diff line change
@@ -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('Aliases', () => {
Expand All @@ -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 typeInto(labelInput, 'test_label');

const idInput = await byTestId('alias_id_input');
await idInput.setValue('test_id');
await typeInto(idInput, 'test_id');

const confirmButton = await byText('Add');
await confirmButton.click();
Expand All @@ -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 typeInto(label0, 'test_label_2');

const id0 = await byTestId('ID_input_0');
await id0.waitForDisplayed({ timeout: 5_000 });
await id0.setValue('test_id_2');
await typeInto(id0, 'test_id_2');

const label1 = await byTestId('Label_input_1');
await label1.waitForDisplayed({ timeout: 5_000 });
await label1.setValue('test_label_3');
await typeInto(label1, 'test_label_3');

const id1 = await byTestId('ID_input_1');
await id1.waitForDisplayed({ timeout: 5_000 });
await id1.setValue('test_id_3');
await typeInto(id1, 'test_id_3');

const confirmButton = await byText('Add All');
await confirmButton.click();
Expand Down
4 changes: 2 additions & 2 deletions appium/tests/specs/05_email.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions appium/tests/specs/06_sms.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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();
Expand Down
22 changes: 14 additions & 8 deletions appium/tests/specs/07_tag.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
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', () => {
Expand All @@ -18,10 +24,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();
Expand All @@ -43,20 +49,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();
Expand All @@ -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');
Expand Down
10 changes: 5 additions & 5 deletions appium/tests/specs/08_outcome.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down
Loading
Loading