Handling System Alerts and Permission Dialogs in Java

6 min read

Both iOS and Android show system-level dialogs that appear outside the app's UI hierarchy: permission requests, push notification prompts, location access requests, and in-app update dialogs. Tests that don't handle these dialogs hang indefinitely waiting for elements that will never appear because a dialog is blocking the screen.

Android permission dialogs

Android permission dialogs appear when the app calls requestPermissions(). They render in a separate process (com.android.permissioncontroller), not in the app's view hierarchy.

Option 1: Auto-grant at capability level (preferred for most tests)

UiAutomator2Options options = new UiAutomator2Options()
    .setAutoGrantPermissions(true);

This grants all permissions listed in the app's manifest before any test runs. Use this when permission flows aren't under test.

Option 2: Handle the dialog via UIAutomator

public void grantPermission() {
    try {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(4));
        WebElement allowButton = wait.until(d ->
            d.findElement(AppiumBy.androidUIAutomator(
                "new UiSelector().text(\"Allow\")"
            ))
        );
        allowButton.click();
    } catch (TimeoutException e) {
        // No permission dialog shown
    }
}

The dialog text varies by Android version: "Allow" (API 23-28), "Allow only while using the app" (API 29+), "While using the app" (API 30+). Use textContains for partial matches:

AppiumBy.androidUIAutomator("new UiSelector().textContains(\"Allow\")")

Option 3: Handle via ADB (fastest for pre-test setup)

// Grant a specific permission without triggering the dialog
((AndroidDriver) driver).executeScript(
    "mobile: shell",
    Map.of(
        "command", "pm grant com.example.app android.permission.ACCESS_FINE_LOCATION"
    )
);

iOS permission alerts

iOS permission dialogs are native alerts. Access them through driver.switchTo().alert():

public void handlePermissionAlert(boolean allow) {
    try {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(4));
        wait.until(d -> {
            try {
                d.switchTo().alert();
                return true;
            } catch (NoAlertPresentException e) {
                return false;
            }
        });
 
        Alert alert = driver.switchTo().alert();
        if (allow) {
            alert.accept();  // "Allow" button
        } else {
            alert.dismiss(); // "Don't Allow" button
        }
    } catch (TimeoutException e) {
        // No alert present
    }
}

Auto-accept all iOS alerts:

XCUITestOptions options = new XCUITestOptions()
    .setAutoAcceptAlerts(true);

Like autoGrantPermissions on Android, use this only when permission behaviour isn't under test.

iOS permission buttons by label

Alert buttons are platform-defined text. Common iOS permission button labels:

DialogAccept labelDeny label
Location"Allow While Using App""Don't Allow"
Camera"OK""Don't Allow"
Notifications"Allow""Don't Allow"
Contacts"OK""Don't Allow"

To tap a specific button by label rather than accept/dismiss (which maps to the first/second button):

driver.findElement(AppiumBy.iOSNsPredicateString(
    "type == 'XCUIElementTypeButton' AND label == 'Allow While Using App'"
)).click();

Location permission dialog (iOS 14+)

iOS 14 added a third option: "Allow Once". Handle it explicitly:

public enum LocationPermission { ALWAYS, WHILE_USING, ONCE, DENY }
 
public void grantLocationPermission(LocationPermission permission) {
    String label = switch (permission) {
        case ALWAYS -> "Always Allow";
        case WHILE_USING -> "Allow While Using App";
        case ONCE -> "Allow Once";
        case DENY -> "Don't Allow";
    };
 
    new WebDriverWait(driver, Duration.ofSeconds(5))
        .until(ExpectedConditions.elementToBeClickable(
            AppiumBy.iOSNsPredicateString("type == 'XCUIElementTypeButton' AND label == '" + label + "'")
        ))
        .click();
}

Push notification permission (iOS)

Push notification prompts appear on first app launch on real devices. They don't appear on simulators before iOS 16. For real device test runs, dismiss the prompt at the start of each session:

@BeforeMethod
public void dismissNotificationPrompt() {
    try {
        new WebDriverWait(driver, Duration.ofSeconds(3))
            .until(d -> d.findElement(
                AppiumBy.iOSNsPredicateString("label == 'Allow' OR label == 'Don't Allow'")
            ));
        // Tap Don't Allow — tests shouldn't rely on push notification delivery
        driver.findElement(AppiumBy.iOSNsPredicateString("label == 'Don't Allow'")).click();
    } catch (TimeoutException e) {
        // No notification prompt — first launch already handled or simulator
    }
}

System alerts in a helper method

Centralise alert handling to avoid repetition:

public class AlertHandler {
 
    private final AppiumDriver driver;
 
    public AlertHandler(AppiumDriver driver) {
        this.driver = driver;
    }
 
    public void dismissIfPresent(Duration timeout) {
        try {
            new WebDriverWait(driver, timeout).until(d -> {
                try {
                    d.switchTo().alert().dismiss();
                    return true;
                } catch (NoAlertPresentException e) {
                    return false;
                }
            });
        } catch (TimeoutException e) {
            // No alert
        }
    }
 
    public void acceptIfPresent(Duration timeout) {
        try {
            new WebDriverWait(driver, timeout).until(d -> {
                try {
                    d.switchTo().alert().accept();
                    return true;
                } catch (NoAlertPresentException e) {
                    return false;
                }
            });
        } catch (TimeoutException e) {
            // No alert
        }
    }
}

Call alertHandler.dismissIfPresent(Duration.ofSeconds(2)) at the start of any test that might encounter a first-run dialog.

// tip to track lessons you complete and pick up where you left off across devices.