Handling System Alerts and Permissions in Python

6 min read

Both iOS and Android show system-level dialogs outside the app's UI hierarchy. Permission requests, push notification prompts, and location access dialogs block test execution if unhandled. This lesson covers how to accept, deny, or auto-dismiss them in Python.

Android permission dialogs

Option 1: Auto-grant at capability level

from appium.options import UiAutomator2Options
 
options = UiAutomator2Options()
options.auto_grant_permissions = True

This grants all permissions before any tests run. Use this when permission flows aren't under test.

Option 2: Handle via UIAutomator

from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
 
 
def grant_permission(driver, timeout=4):
    """Taps 'Allow' on an Android permission dialog if present."""
    try:
        allow_btn = WebDriverWait(driver, timeout).until(
            lambda d: d.find_element(
                AppiumBy.ANDROID_UIAUTOMATOR,
                'new UiSelector().textContains("Allow")'
            )
        )
        allow_btn.click()
    except TimeoutException:
        pass  # No dialog shown
 
 
def deny_permission(driver, timeout=4):
    try:
        deny_btn = WebDriverWait(driver, timeout).until(
            lambda d: d.find_element(
                AppiumBy.ANDROID_UIAUTOMATOR,
                'new UiSelector().textContains("Don\'t allow")'
            )
        )
        deny_btn.click()
    except TimeoutException:
        pass

Option 3: Grant via ADB shell

def grant_permission_adb(driver, package: str, permission: str):
    """Grants a permission without triggering the dialog UI."""
    driver.execute_script("mobile: shell", {
        "command": f"pm grant {package} {permission}"
    })
 
# Usage
grant_permission_adb(
    driver,
    "com.example.myapp",
    "android.permission.ACCESS_FINE_LOCATION"
)

iOS permission alerts

iOS permissions appear as native alerts, accessible via driver.switch_to.alert:

from selenium.common.exceptions import NoAlertPresentException, TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
 
 
def accept_alert(driver, timeout=4):
    """Accept an iOS system alert if present."""
    try:
        WebDriverWait(driver, timeout).until(
            lambda d: _try_accept(d)
        )
    except TimeoutException:
        pass
 
 
def dismiss_alert(driver, timeout=4):
    """Dismiss an iOS system alert if present."""
    try:
        WebDriverWait(driver, timeout).until(
            lambda d: _try_dismiss(d)
        )
    except TimeoutException:
        pass
 
 
def _try_accept(driver) -> bool:
    try:
        driver.switch_to.alert.accept()
        return True
    except NoAlertPresentException:
        return False
 
 
def _try_dismiss(driver) -> bool:
    try:
        driver.switch_to.alert.dismiss()
        return True
    except NoAlertPresentException:
        return False

Auto-accept all iOS alerts:

from appium.options import XCUITestOptions
 
options = XCUITestOptions()
options.auto_accept_alerts = True

iOS permission buttons by label

Different iOS permission dialogs have different button labels. Tap by label directly:

from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
 
 
def tap_permission_button(driver, label: str, timeout=5):
    """Taps an iOS permission dialog button by its exact label."""
    try:
        WebDriverWait(driver, timeout).until(
            lambda d: d.find_element(
                AppiumBy.IOS_PREDICATE,
                f"type == 'XCUIElementTypeButton' AND label == '{label}'"
            )
        ).click()
    except TimeoutException:
        pass  # Button not shown
 
 
# Usage
tap_permission_button(driver, "Allow While Using App")     # Location
tap_permission_button(driver, "Allow")                      # Camera/notifications
tap_permission_button(driver, "Don't Allow")               # Deny
tap_permission_button(driver, "Allow Once")                # iOS 14+ one-time location

Push notification prompt (iOS)

Push prompts appear on real devices on first launch. Simulators before iOS 16 don't show them.

def dismiss_notification_prompt(driver):
    """Dismiss iOS push notification permission dialog if shown."""
    # Try "Don't Allow" first — tests should not rely on push delivery
    tap_permission_button(driver, "Don't Allow", timeout=3)
    if not tap_permission_button.__doc__:  # if nothing was tapped
        # Some versions show "Disallow"
        tap_permission_button(driver, "Disallow", timeout=1)

AlertHandler class

Centralise alert handling in a reusable class:

class AlertHandler:
    def __init__(self, driver):
        self.driver = driver
 
    def accept_if_present(self, timeout=3):
        accept_alert(self.driver, timeout)
        return self
 
    def dismiss_if_present(self, timeout=3):
        dismiss_alert(self.driver, timeout)
        return self
 
    def grant_android_permission(self, timeout=4):
        grant_permission(self.driver, timeout)
        return self
 
    def deny_android_permission(self, timeout=4):
        deny_permission(self.driver, timeout)
        return self
 
    def tap_ios_button(self, label: str, timeout=4):
        tap_permission_button(self.driver, label, timeout)
        return self

Usage in page objects:

class LocationPage(BasePage):
    ENABLE_LOCATION = (AppiumBy.ACCESSIBILITY_ID, "enableLocation")
 
    def enable_location(self):
        self.tap(self.ENABLE_LOCATION)
        # Handle the system permission dialog
        alerts = AlertHandler(self.driver)
        alerts.tap_ios_button("Allow While Using App").grant_android_permission()
        return self

Fixture: handle startup dialogs

For suites where every test starts fresh, handle startup dialogs in a fixture rather than in individual page objects:

@pytest.fixture
def clean_driver(driver):
    """Returns a driver with startup dialogs already dismissed."""
    accept_alert(driver, timeout=3)      # iOS notification prompt
    grant_permission(driver, timeout=3)  # Android location or camera
    yield driver

Tests use clean_driver instead of driver when they don't care about permission flows:

def test_login(clean_driver):
    LoginPage(clean_driver).login("user", "pass")
    # No permission dialog to handle

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