Skip to content

Commit 3c535d9

Browse files
authored
test: [SDK-4333] fix Appium tests for Flutter Android (#30)
1 parent 345c194 commit 3c535d9

16 files changed

Lines changed: 348 additions & 170 deletions

appium/tests/helpers/app.ts

Lines changed: 177 additions & 81 deletions
Large diffs are not rendered by default.

appium/tests/helpers/selectors.ts

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export function getTestData() {
112112
}
113113

114114
export async function deleteUser(externalId: string) {
115-
console.log(`Deleting user: ${externalId}`);
115+
console.info(`Deleting user: ${externalId}`);
116116
try {
117117
const response = await fetch(
118118
`https://api.onesignal.com/apps/${process.env.ONESIGNAL_APP_ID}/users/by/external_id/${externalId}`,
@@ -127,12 +127,21 @@ export async function deleteUser(externalId: string) {
127127
if (!response.ok) {
128128
throw new Error(`Failed to delete user: ${response.statusText}`);
129129
}
130-
console.log(`User deleted successfully`);
130+
console.info(`User deleted successfully`);
131131
} catch (error) {
132132
console.error(`Failed to delete user: ${error}`);
133133
}
134134
}
135135

136+
export async function getToggleState(el: {
137+
getAttribute(name: string): Promise<string | null>;
138+
}): Promise<boolean> {
139+
if (getPlatform() === 'ios') {
140+
return (await el.getAttribute('value')) === '1';
141+
}
142+
return (await el.getAttribute('checked')) === 'true';
143+
}
144+
136145
export function getSdkType(): SdkType {
137146
const sdkType = process.env.SDK_TYPE;
138147
if (sdkType && VALID_SDK_TYPES.has(sdkType)) {
@@ -143,28 +152,70 @@ export function getSdkType(): SdkType {
143152
);
144153
}
145154

155+
/**
156+
* On Flutter Android, the standard WebDriver getText() often returns empty
157+
* because Flutter writes text into content-desc / text attributes rather than
158+
* the property that UiAutomator2's getText maps to. This proxy intercepts
159+
* getText() and falls back to those attributes.
160+
*/
161+
function withFlutterAndroidFixes<T extends { getText(): Promise<string> }>(el: T): T {
162+
if (!(getPlatform() === 'android' && getSdkType() === 'flutter')) {
163+
return el;
164+
}
165+
166+
return new Proxy(el, {
167+
get(target, prop, receiver) {
168+
if (prop === 'getText') {
169+
return async () => {
170+
const text = (await target.getText()).trim();
171+
if (text) return text;
172+
173+
const attrs = ['content-desc', 'contentDescription', 'text', 'name'];
174+
for (const attr of attrs) {
175+
try {
176+
const val = (
177+
await (
178+
target as unknown as { getAttribute(n: string): Promise<string | null> }
179+
).getAttribute(attr)
180+
)?.trim();
181+
if (val) return val;
182+
} catch {
183+
/* best-effort */
184+
}
185+
}
186+
187+
return '';
188+
};
189+
}
190+
191+
const value = Reflect.get(target, prop, receiver);
192+
if (typeof value === 'function') {
193+
return value.bind(target);
194+
}
195+
196+
return value;
197+
},
198+
});
199+
}
200+
146201
/**
147202
* Select an element by its cross-platform test ID.
148203
*
149204
* Native iOS uses `accessibilityIdentifier`, native Android Compose uses
150-
* `testTag`, RN uses `testID`, and Flutter uses `Semantics(label:)` — all
151-
* map to Appium accessibility id. Capacitor uses `data-testid` as a CSS
152-
* attribute inside a WebView.
205+
* `testTag`, RN uses `testID` — all map to Appium accessibility id (`~`).
206+
* Flutter uses `Semantics(identifier:)` which maps to `accessibilityIdentifier`
207+
* on iOS (`~`) but to `resource-id` on Android (UiAutomator selector).
208+
* Capacitor uses `data-testid` as a CSS attribute inside a WebView.
153209
*/
154210
export async function byTestId(id: string) {
155211
const sdkType = getSdkType();
156-
switch (sdkType) {
157-
case 'react-native':
158-
case 'flutter':
159-
case 'unity':
160-
case 'cordova':
161-
case 'dotnet':
162-
case 'ios':
163-
case 'android':
164-
return $(`~${id}`);
165-
case 'capacitor':
166-
return $(`[data-testid="${id}"]`);
167-
}
212+
const platform = getPlatform();
213+
214+
if (sdkType === 'capacitor') return $(`[data-testid="${id}"]`);
215+
if (sdkType === 'flutter' && platform === 'android')
216+
return withFlutterAndroidFixes(await $(`id=${id}`));
217+
218+
return $(`~${id}`);
168219
}
169220

170221
/**
@@ -173,16 +224,17 @@ export async function byTestId(id: string) {
173224
*/
174225
export async function byText(text: string, partial = false) {
175226
const platform = getPlatform();
176-
const sdkType = getSdkType();
177-
178-
if (sdkType === 'capacitor') {
179-
return $(`//*[contains(text(), "${text}")]`);
180-
}
181227

182228
if (platform === 'ios') {
183229
const op = partial ? 'CONTAINS' : '==';
184230
return $(`-ios predicate string:label ${op} "${text}"`);
185231
}
186-
const method = partial ? 'textContains' : 'text';
187-
return $(`android=new UiSelector().${method}("${text}")`);
232+
233+
if (partial) {
234+
return withFlutterAndroidFixes(
235+
await $(`//*[contains(@content-desc, "${text}") or contains(@text, "${text}")]`),
236+
);
237+
}
238+
239+
return withFlutterAndroidFixes(await $(`//*[@content-desc="${text}" or @text="${text}"]`));
188240
}

appium/tests/specs/01_user.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ describe('User', () => {
77
await scrollToEl('user_section');
88
});
99

10+
after(async () => {
11+
// login user back so we can clean up the user data for the next run
12+
await driver.pause(3_000);
13+
await waitForAppReady();
14+
});
15+
1016
it('should start as anonymous', async () => {
1117
const statusEl = await scrollToEl('user_status_value');
1218
const status = await statusEl.getText();
@@ -29,6 +35,7 @@ describe('User', () => {
2935
const externalId = await externalIdEl.getText();
3036
expect(externalId).toBe(getTestExternalId());
3137

38+
await driver.pause(3_000);
3239
await logoutUser();
3340

3441
statusEl = await scrollToEl('user_status_value');

appium/tests/specs/03_iam.spec.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
scrollToEl,
66
waitForAppReady,
77
} from '../helpers/app';
8+
import { getToggleState } from '../helpers/selectors';
89

910
describe('In-App Messaging', () => {
1011
before(async () => {
@@ -33,18 +34,21 @@ describe('In-App Messaging', () => {
3334
it('can pause iam', async () => {
3435
const toggle = await scrollToEl('Pause In-App', { by: 'text', partial: true, direction: 'up' });
3536

36-
expect(await toggle.getAttribute('value')).toBe('0');
37-
await toggle.click({ x: 0, y: 0 });
38-
expect(await toggle.getAttribute('value')).toBe('1');
37+
expect(await getToggleState(toggle)).toBe(false);
38+
await toggle.click();
39+
expect(await getToggleState(toggle)).toBe(true);
3940

4041
// try to show top banner, should fail since IAM is paused
4142
const button = await scrollToEl('TOP BANNER', { by: 'text' });
4243
await button.click();
4344
await driver.pause(3_000);
44-
expect(await isWebViewVisible()).toBe(false);
45+
46+
if (driver.isIOS) {
47+
expect(await isWebViewVisible()).toBe(false);
48+
}
4549

4650
// reset back
47-
await toggle.click({ x: 0, y: 0 });
51+
await toggle.click();
4852
await checkInAppMessage({
4953
buttonLabel: 'TOP BANNER',
5054
expectedTitle: 'Top Banner',

appium/tests/specs/04_alias.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { checkTooltip, expectPairInSection, scrollToEl, waitForAppReady } from '../helpers/app';
1+
import { checkTooltip, expectPairInSection, scrollToEl, typeInto, waitForAppReady } from '../helpers/app';
22
import { byTestId, byText } from '../helpers/selectors.js';
33

44
describe('Aliases', () => {
@@ -17,10 +17,10 @@ describe('Aliases', () => {
1717

1818
const labelInput = await byTestId('alias_label_input');
1919
await labelInput.waitForDisplayed({ timeout: 5_000 });
20-
await labelInput.setValue('test_label');
20+
await typeInto(labelInput, 'test_label');
2121

2222
const idInput = await byTestId('alias_id_input');
23-
await idInput.setValue('test_id');
23+
await typeInto(idInput, 'test_id');
2424

2525
const confirmButton = await byText('Add');
2626
await confirmButton.click();
@@ -37,19 +37,19 @@ describe('Aliases', () => {
3737

3838
const label0 = await byTestId('Label_input_0');
3939
await label0.waitForDisplayed({ timeout: 5_000 });
40-
await label0.setValue('test_label_2');
40+
await typeInto(label0, 'test_label_2');
4141

4242
const id0 = await byTestId('ID_input_0');
4343
await id0.waitForDisplayed({ timeout: 5_000 });
44-
await id0.setValue('test_id_2');
44+
await typeInto(id0, 'test_id_2');
4545

4646
const label1 = await byTestId('Label_input_1');
4747
await label1.waitForDisplayed({ timeout: 5_000 });
48-
await label1.setValue('test_label_3');
48+
await typeInto(label1, 'test_label_3');
4949

5050
const id1 = await byTestId('ID_input_1');
5151
await id1.waitForDisplayed({ timeout: 5_000 });
52-
await id1.setValue('test_id_3');
52+
await typeInto(id1, 'test_id_3');
5353

5454
const confirmButton = await byText('Add All');
5555
await confirmButton.click();

appium/tests/specs/05_email.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { checkTooltip, scrollToEl, waitForAppReady } from '../helpers/app';
1+
import { checkTooltip, scrollToEl, typeInto, waitForAppReady } from '../helpers/app';
22
import { byTestId, byText, getTestData } from '../helpers/selectors.js';
33

44
describe('Emails', () => {
@@ -20,7 +20,7 @@ describe('Emails', () => {
2020

2121
const emailInput = await byTestId('Email_input');
2222
await emailInput.waitForDisplayed({ timeout: 5_000 });
23-
await emailInput.setValue(email);
23+
await typeInto(emailInput, email);
2424

2525
const confirmButton = await byText('Add');
2626
await confirmButton.click();

appium/tests/specs/06_sms.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { checkTooltip, scrollToEl, waitForAppReady } from '../helpers/app';
1+
import { checkTooltip, scrollToEl, typeInto, waitForAppReady } from '../helpers/app';
22
import { byTestId, byText, getTestData } from '../helpers/selectors.js';
33

44
describe('SMS', () => {
@@ -19,7 +19,7 @@ describe('SMS', () => {
1919

2020
const smsInput = await byTestId('SMS Number_input');
2121
await smsInput.waitForDisplayed({ timeout: 5_000 });
22-
await smsInput.setValue(sms);
22+
await typeInto(smsInput, sms);
2323

2424
const confirmButton = await byText('Add');
2525
await confirmButton.click();

appium/tests/specs/07_tag.spec.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { checkTooltip, expectPairInSection, scrollToEl, waitForAppReady } from '../helpers/app';
1+
import {
2+
checkTooltip,
3+
expectPairInSection,
4+
scrollToEl,
5+
typeInto,
6+
waitForAppReady,
7+
} from '../helpers/app';
28
import { byTestId, byText } from '../helpers/selectors.js';
39

410
describe('Tags', () => {
@@ -18,10 +24,10 @@ describe('Tags', () => {
1824
// add tag
1925
const keyInput = await byTestId('tag_key_input');
2026
await keyInput.waitForDisplayed({ timeout: 5_000 });
21-
await keyInput.setValue('test_tag');
27+
await typeInto(keyInput, 'test_tag');
2228

2329
const valueInput = await byTestId('tag_value_input');
24-
await valueInput.setValue('test_tag_value');
30+
await typeInto(valueInput, 'test_tag_value');
2531

2632
const confirmButton = await byTestId('tag_confirm_button');
2733
await confirmButton.click();
@@ -43,20 +49,20 @@ describe('Tags', () => {
4349
// add tags
4450
const key0 = await byTestId('Key_input_0');
4551
await key0.waitForDisplayed({ timeout: 5_000 });
46-
await key0.setValue('test_tag_2');
52+
await typeInto(key0, 'test_tag_2');
4753

4854
const value0 = await byTestId('Value_input_0');
49-
await value0.setValue('test_tag_value_2');
55+
await typeInto(value0, 'test_tag_value_2');
5056

5157
const addRowButton = await byText('Add Row');
5258
await addRowButton.click();
5359

5460
const key1 = await byTestId('Key_input_1');
5561
await key1.waitForDisplayed({ timeout: 5_000 });
56-
await key1.setValue('test_tag_3');
62+
await typeInto(key1, 'test_tag_3');
5763

5864
const value1 = await byTestId('Value_input_1');
59-
await value1.setValue('test_tag_value_3');
65+
await typeInto(value1, 'test_tag_value_3');
6066

6167
let confirmButton = await byText('Add All');
6268
await confirmButton.click();
@@ -65,7 +71,7 @@ describe('Tags', () => {
6571
await expectPairInSection('tags', 'test_tag_3', 'test_tag_value_3');
6672

6773
// remove tags
68-
const removeButton = await scrollToEl('REMOVE TAGS');
74+
const removeButton = await scrollToEl('REMOVE TAGS', { by: 'text' });
6975
await removeButton.click();
7076

7177
const tag2Checkbox = await byTestId('remove_checkbox_test_tag_2');

appium/tests/specs/08_outcome.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { checkTooltip, scrollToEl, waitForAppReady } from '../helpers/app';
1+
import { checkTooltip, scrollToEl, typeInto, waitForAppReady } from '../helpers/app';
22
import { byTestId, byText } from '../helpers/selectors.js';
33

44
describe('Outcomes', () => {
@@ -17,7 +17,7 @@ describe('Outcomes', () => {
1717

1818
const nameInput = await byTestId('outcome_name_input');
1919
await nameInput.waitForDisplayed({ timeout: 5_000 });
20-
await nameInput.setValue('test_normal');
20+
await typeInto(nameInput, 'test_normal');
2121

2222
const normalRadio = await byText('Normal Outcome');
2323
await normalRadio.click();
@@ -35,7 +35,7 @@ describe('Outcomes', () => {
3535

3636
const nameInput = await byTestId('outcome_name_input');
3737
await nameInput.waitForDisplayed({ timeout: 5_000 });
38-
await nameInput.setValue('test_unique');
38+
await typeInto(nameInput, 'test_unique');
3939

4040
const uniqueRadio = await byText('Unique Outcome');
4141
await uniqueRadio.click();
@@ -57,11 +57,11 @@ describe('Outcomes', () => {
5757
const withValueRadio = await byText('Outcome with Value');
5858
await withValueRadio.click();
5959

60-
await nameInput.setValue('test_valued');
60+
await typeInto(nameInput, 'test_valued');
6161

6262
const valueInput = await byTestId('outcome_value_input');
6363
await valueInput.waitForDisplayed({ timeout: 5_000 });
64-
await valueInput.setValue('3.14');
64+
await typeInto(valueInput, '3.14');
6565

6666
const sendBtn = await byTestId('outcome_send_button');
6767
await sendBtn.click();

0 commit comments

Comments
 (0)