Skip to content

Commit 5a666ca

Browse files
committed
test(appium): auto-accept system permission dialogs
1 parent 3bd1bbc commit 5a666ca

4 files changed

Lines changed: 14 additions & 127 deletions

File tree

appium/tests/helpers/app.ts

Lines changed: 5 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async function stopScrolling() {
1515
if (platform === 'android') {
1616
// Android's scrollGesture already completes the gesture. A follow-up tap in
1717
// the center of the screen can hit interactive elements like LOGIN USER.
18-
await driver.pause(150);
18+
// await driver.pause(150);
1919
return;
2020
}
2121

@@ -114,120 +114,16 @@ export async function scrollToEl(
114114
throw new Error(`Element "${identifier}" not found after ${maxScrolls} scrolls`);
115115
}
116116

117-
/**
118-
* Wait for an iOS system alert to appear and return its text without
119-
* dismissing it. Returns null if no alert appears within the timeout.
120-
* iOS-only — used by the location spec which needs to accept with a
121-
* specific button label.
122-
*/
123-
export async function waitForAlert(timeoutMs = 10_000): Promise<string | null> {
124-
try {
125-
await driver.waitUntil(
126-
async () => {
127-
try {
128-
const buttons = await driver.execute('mobile: alert', { action: 'getButtons' });
129-
return Array.isArray(buttons) && buttons.length > 0;
130-
} catch {
131-
return false;
132-
}
133-
},
134-
{ timeout: timeoutMs, interval: 250 },
135-
);
136-
return await driver.getAlertText();
137-
} catch {
138-
return null;
139-
}
140-
}
141-
142-
/**
143-
* Wait for a native system alert/permission dialog, accept it, and return
144-
* its text. Returns null if no dialog appears within the timeout.
145-
*
146-
* iOS: uses XCUITest `mobile: alert` API.
147-
* Android: looks for the standard permission dialog "Allow" button via
148-
* UiAutomator (works for POST_NOTIFICATIONS, location, etc.). Handles
149-
* stock Android, Samsung (`com.samsung.android.permissioncontroller`),
150-
* and legacy `com.android.packageinstaller` package variants.
151-
*/
152-
export async function acceptSystemAlert(timeoutMs = 10_000): Promise<string | null> {
153-
const platform = getPlatform();
154-
155-
try {
156-
if (platform === 'ios') {
157-
const text = await waitForAlert(timeoutMs);
158-
if (text) await driver.acceptAlert();
159-
return text;
160-
}
161-
162-
// Try resource-id first (most robust across OEMs), then fall back to
163-
// text match. "Don't allow" contains "Allow" as a substring, so we
164-
// must use exact `text()` rather than `textContains()`.
165-
const allowSelectors = [
166-
'new UiSelector().resourceIdMatches(".*:id/permission_allow_button")',
167-
'new UiSelector().resourceIdMatches(".*:id/permission_allow_foreground_only_button")',
168-
'new UiSelector().resourceIdMatches(".*:id/permission_allow_one_time_button")',
169-
'new UiSelector().text("Allow")',
170-
'new UiSelector().text("ALLOW")',
171-
'new UiSelector().text("While using the app")',
172-
];
173-
174-
let allowBtn: Awaited<ReturnType<typeof $>> | null = null;
175-
const deadline = Date.now() + timeoutMs;
176-
while (Date.now() < deadline) {
177-
for (const sel of allowSelectors) {
178-
const el = await $(`android=${sel}`);
179-
if (await el.isDisplayed().catch(() => false)) {
180-
allowBtn = el;
181-
break;
182-
}
183-
}
184-
if (allowBtn) break;
185-
await driver.pause(250);
186-
}
187-
188-
if (!allowBtn) return null;
189-
190-
let text = 'Permission dialog';
191-
try {
192-
const msgEl = await $(
193-
'android=new UiSelector().resourceIdMatches(".*:id/permission_message")',
194-
);
195-
if (await msgEl.isDisplayed().catch(() => false)) {
196-
text = await msgEl.getText();
197-
}
198-
} catch {
199-
/* best-effort */
200-
}
201-
await allowBtn.click();
202-
return text;
203-
} catch {
204-
return null;
205-
}
206-
}
207-
208-
async function acceptSystemAlerts(timeoutMs: number): Promise<void> {
209-
await browser.waitUntil(
210-
async () => {
211-
const alertText = await acceptSystemAlert(500);
212-
return !alertText;
213-
},
214-
{ timeout: timeoutMs, interval: 500 },
215-
);
216-
}
217-
218117
/**
219118
* Wait for the app to fully launch and the home screen to be visible.
119+
*
120+
* System permission dialogs are handled automatically by Appium via the
121+
* `autoGrantPermissions` (Android) and `autoAcceptAlerts` (iOS) capabilities,
122+
* so no explicit dialog handling is needed here.
220123
*/
221124
export async function waitForAppReady(opts: { skipLogin?: boolean } = {}) {
222125
const { skipLogin = false } = opts;
223126

224-
const alertHandled = await browser.sharedStore.get('alertHandled');
225-
if (!alertHandled) {
226-
// Accept permission dialogs until the app UI is visible.
227-
await acceptSystemAlerts(5_000);
228-
await browser.sharedStore.set('alertHandled', true);
229-
}
230-
231127
const mainScroll = await byTestId('main_scroll_view');
232128
await mainScroll.waitForDisplayed({ timeout: 5_000 });
233129

appium/tests/specs/11_location.spec.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { waitForAppReady, waitForAlert, scrollToEl, checkTooltip } from '../helpers/app.js';
2-
import { byText, getPlatform } from '../helpers/selectors.js';
1+
import { waitForAppReady, scrollToEl, checkTooltip, acceptAlert } from '../helpers/app.js';
2+
import { byText } from '../helpers/selectors.js';
33

44
describe('Location', () => {
55
before(async () => {
@@ -11,24 +11,15 @@ describe('Location', () => {
1111
await checkTooltip('location_info_icon', 'location');
1212
});
1313

14+
// Not an ideal test since we auto accept the system permission dialog
15+
// If we have observable callbacks for ios & android, we can test this better
1416
it('can prompt for location', async () => {
17+
// The system permission dialog is auto-accepted by Appium
18+
// (`autoGrantPermissions` on Android, `autoAcceptAlerts` on iOS),
19+
// so we just trigger the prompt and let Appium handle it.
1520
const promptButton = await scrollToEl('PROMPT LOCATION', { by: 'text' });
1621
await promptButton.click();
17-
1822
await driver.pause(3_000);
19-
20-
if (getPlatform() === 'ios') {
21-
const alert = await waitForAlert();
22-
expect(alert).toContain('location');
23-
await driver.execute('mobile: alert', {
24-
action: 'accept',
25-
buttonLabel: 'Allow While Using App',
26-
});
27-
} else {
28-
const allowBtn = await byText('While using the app');
29-
await allowBtn.waitForDisplayed({ timeout: 10_000 });
30-
await allowBtn.click();
31-
}
3223
});
3324

3425
it('can share location', async () => {

appium/wdio.android.conf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const config: WebdriverIO.Config = {
1212
'appium:platformVersion': process.env.OS_VERSION || '16.0',
1313
'appium:automationName': 'UiAutomator2',
1414
...(process.env.BUNDLE_ID ? { 'appium:appPackage': process.env.BUNDLE_ID } : {}),
15-
'appium:autoGrantPermissions': false,
15+
'appium:autoGrantPermissions': true,
1616
'appium:noReset': true,
1717
...(isLocal ? {} : { 'bstack:options': bstackOptions }),
1818

appium/wdio.ios.conf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const config: WebdriverIO.Config = {
1212
'appium:platformVersion': process.env.OS_VERSION || '26',
1313
'appium:automationName': 'XCUITest',
1414
...(process.env.BUNDLE_ID ? { 'appium:bundleId': process.env.BUNDLE_ID } : {}),
15-
'appium:autoAcceptAlerts': false,
15+
'appium:autoAcceptAlerts': true,
1616
'appium:noReset': true,
1717
...(isLocal ? {} : { 'bstack:options': bstackOptions }),
1818

0 commit comments

Comments
 (0)