Back to blog
    August 23, 2026Tutorial

    Auto-Accept Permission Dialogs at Session Start — Then Turn It Off Mid-Test

    Every mobile suite hits this on day one. The app launches, iOS asks for notifications, then location, then tracking, and every test that isn't about permissions dies on a dialog it never asked for.

    So you set appium:autoAcceptAlerts and the suite goes green.

    Then someone writes the one test that is about permissions — the one that asserts the location prompt appears, taps Don't Allow, and checks the empty state — and discovers that the capability that saved the suite is now the thing blocking it. The alert is gone before the test can see it.

    Here's the accurate picture of what's session-scoped, what's runtime-toggleable, and what to reach for on each platform.

    Layer 1: the session capabilities

    These are set at session creation. They're the ones everyone reaches for first — and, as the next section shows, they are not the layer you actually want to be controlling.

    iOS (XCUITest). appium:autoAcceptAlerts — "Accept all iOS alerts automatically if they pop up. This includes privacy access permission alerts (location, contacts, photos). Default is false." appium:autoDismissAlerts is the mirror image. Note what the wording covers: system alerts, including privacy prompts, not just your app's own UIAlertController.

    Android (UiAutomator2). There's no autoAcceptAlerts equivalent, because Android runtime permissions aren't really alerts you accept — they're grants you can hand over before the dialog ever appears. That's appium:autoGrantPermissions: "Whether to grant all the requested application permissions automatically when a test starts."

    Three constraints on it that bite in practice:

    • targetSdkVersion in the manifest must be ≥ 23 and the device must be Android 6+.
    • Apps with targetSdkVersion ≤ 22 "must be reinstalled to grant permissions" — e.g. with appium:fullReset.
    • Grants happen at install time. If your session reuses an already-installed app, the capability has nothing to act on. This is the single most common reason people report that "autoGrantPermissions doesn't work."

    And for special permissions — notifications access, media recording — the plain grant path doesn't cover them; the docs point you at mobile: changePermissions with the appops target instead. More on that below.

    One iOS caveat worth internalising: the appium:permissions capability, the one that lets you preset per-service permission states, "Allows to set permissions for the specified application bundle on Simulator only." On a real iPhone it isn't available. Real-device iOS permission state is something you handle through dialogs or through how the device was left.

    The two settings that actually control it

    The capabilities are not the mechanism. They're a convenience that seeds one, and the mechanism underneath is runtime-mutable — which means the mid-session toggle everyone assumes is impossible is in fact two lines of code.

    When the driver builds its underlying iOS automation session, it does exactly this:

    js
    if (driver.opts.autoAcceptAlerts) {
      wdaCaps.defaultAlertAction = 'accept';
    } else if (driver.opts.autoDismissAlerts) {
      wdaCaps.defaultAlertAction = 'dismiss';
    }

    So appium:autoAcceptAlerts is a front end for a setting called defaultAlertAction. And defaultAlertAction is registered in the settings map, so it can be written any time during the session. The driver forwards any setting key it doesn't handle specially straight to the agent's settings endpoint, so a plain setSettings call reaches it:

    java
    // Turn auto-accept off, mid-session
    getAppiumDriver().setSettings(ImmutableMap.of("defaultAlertAction", ""));
    
    // ... assert on the dialog, tap what you want ...
    
    // Turn it back on
    getAppiumDriver().setSettings(ImmutableMap.of("defaultAlertAction", "accept"));

    You'll see "off" used for the disable value, and it works — but by accident rather than by design. The handling is a straight comparison against accept and dismiss; anything else falls through to a branch that logs 'off' default alert action is unsupported and does nothing. The behaviour you want, with a warning line per alert. An empty string is the value that's explicitly checked for and returns early with no log noise. (null works too, but ImmutableMap rejects null values, which is exactly why "off" tends to be what people reach for in Java.)

    This is worth knowing precisely because none of it is in the driver's settings documentation — defaultAlertAction isn't listed there, and the Appium forum answer on this question is "remove autoAccept and manage all by yourself." That answer is out of date. The setting is real, it's readable back, and it's the supported shape of the thing.

    autoClickAlertSelector, and why it's checked first

    The second setting is autoClickAlertSelector:

    Custom selector for an alert button. This can be used to automatically locate an element inside an alert hierarchy, and tap it as soon as a new alert is detected.

    This setting takes priority over the appium:autoAcceptAlerts and appium:autoDismissAlerts capabilities.

    "Takes priority" is literal: when an alert appears, the selector is consulted first, and if it's set the handler taps and returns without ever consulting defaultAlertAction — including when the tap fails. It's strictly more expressive than the boolean, because it names which button gets tapped rather than accepting whatever the OS considers the default:

    js
    await driver.updateSettings({
      autoClickAlertSelector: "**/XCUIElementTypeButton[`label CONTAINS[c] 'allow'`]",
    });

    To disable it, set it to an empty string. This isn't a trick — an empty value is special-cased ahead of any parsing, and it additionally tears down the alert monitor rather than merely failing to match:

    java
    getAppiumDriver().setSettings(ImmutableMap.of("autoClickAlertSelector", ""));

    Don't disable it by supplying a valid selector that matches nothing. That leaves the monitor running and pays for a class-chain query against every alert for no benefit. And note that the documented "an error is thrown if the provided selector is invalid" applies only to non-empty values — the parse step is never reached for an empty one.

    Putting the two together

    Because the selector short-circuits the alert action, fully disabling auto-handling means clearing both, in that order:

    java
    private void pauseAlertHandling() {
      getAppiumDriver().setSettings(ImmutableMap.of("autoClickAlertSelector", ""));
      getAppiumDriver().setSettings(ImmutableMap.of("defaultAlertAction", ""));
    }
    
    private void resumeAlertHandling() {
      getAppiumDriver().setSettings(ImmutableMap.of("defaultAlertAction", "accept"));
    }

    Clearing only defaultAlertAction while a selector is still set changes nothing at all, because the selector is what's handling your alerts. That's the failure mode to watch for if you've configured both — the disable appears to do nothing, and it looks like the setting was ignored.

    Wire that pair into @BeforeEach / @AfterEach and the permission-specific tests live in the same session and the same suite as everything else, with no second capability set.

    You can also start the session with the setting instead of the boolean capability, using Appium's appium:settings[...] prefix:

    json
    {
      "platformName": "iOS",
      "appium:automationName": "XCUITest",
      "appium:settings[autoClickAlertSelector]": "**/XCUIElementTypeButton[`label CONTAINS[c] 'allow'`]"
    }

    Same effect as autoAcceptAlerts, but now the behaviour lives somewhere you can change without tearing down the session.

    Layer 2, Android: revoke and re-grant at runtime

    Android's runtime lever isn't about alerts at all — it's about the grant itself. The UiAutomator2 driver's mobile: changePermissions "Changes package permissions in runtime":

    ArgumentValuesNotes
    permissionspermission name, array, or allall works only with target: pm
    appPackagepackage namedefaults to the app under test
    actiongrant (default) / revoke for pm; allow / deny / ignore / default for appops
    targetpm (default) or appopsappops requires the driver's adb_shell server security option

    So the whole test starts with permissions granted, and the one test that needs the dialog revokes it first and lets the app ask:

    java
    // Start clean: revoke, so the next launch actually prompts
    driver.executeScript("mobile: changePermissions", Map.of(
        "permissions", "android.permission.ACCESS_FINE_LOCATION",
        "action", "revoke"));
    
    driver.executeScript("mobile: activateApp", Map.of("appId", "com.mycompany.myapp"));
    // ... assert the system dialog, tap Deny, assert your empty state ...
    
    // Hand it back for the rest of the suite
    driver.executeScript("mobile: changePermissions", Map.of(
        "permissions", "all", "action", "grant"));

    And to assert on state rather than guess at it, mobile: getPermissions takes a type of denied, granted or requested (the default) and returns a list of permission names. That turns "did the grant actually apply?" from a debugging session into an assertion.

    Note the revoke-then-relaunch pattern. Revoking a permission that a running process already holds is not something the app observes gracefully — Android may kill the process. Do it before activating the app, not in the middle of a flow, unless killing the app is the thing you're testing.

    Layer 3: handling one alert by hand

    When you just need to deal with the dialog in front of you:

    iOSmobile: alert takes an action of accept, dismiss or getButtons, plus an optional buttonLabel. getButtons is underused and genuinely handy: assert on the exact button set the OS is offering before you commit to tapping one, which is how you catch iOS-version differences in prompt wording instead of failing on a hardcoded label.

    js
    const buttons = await driver.execute("mobile: alert", { action: "getButtons" });
    // e.g. ["Don't Allow", "Allow Once", "Allow While Using App"]
    await driver.execute("mobile: alert", { action: "accept", buttonLabel: "Allow Once" });

    Androidmobile: acceptAlert and mobile: dismissAlert, both with an optional buttonLabel. The docs are refreshingly honest about the reliability: "This method might not always be reliable as there is no single standard for how Android alerts should look like within the Accessibility representation." Pass an explicit buttonLabel when you know it, and prefer changePermissions over dialog-tapping for anything permission-shaped.

    Two more iOS settings worth knowing while you're in here:

    • acceptAlertButtonSelector / dismissAlertButtonSelector — class chains that change which button the standard W3C Accept Alert / Dismiss Alert commands press. For "handle accept buttons with arbitrary text," i.e. custom in-app dialogs whose buttons say Got it and Maybe later.
    • respectSystemAlerts — "Whether to automatically switch the active application to the system springboard if a native alert element is detected." Default false. If your alert handling works everywhere except when a system prompt overlays your app, turn this on before you start suspecting the driver.

    A policy that holds up

    SituationUse
    Permission prompts are noise in 95% of testsiOS: appium:settings[autoClickAlertSelector]. Android: appium:autoGrantPermissions
    One test must assert on the promptiOS: clear autoClickAlertSelector and defaultAlertAction to "", restore after. Android: mobile: changePermissionsrevoke, then relaunch
    Special permissions (notifications, recording)Android: mobile: changePermissions with target: appops
    Prompt wording varies by OS versionmobile: alertgetButtons, assert, then act
    Custom in-app dialog, non-standard buttonsacceptAlertButtonSelector / dismissAlertButtonSelector
    iOS Simulator, preset state before launchappium:permissions (Simulator only)

    The through-line: prefer the setting over the capability even when both would work today. The capability is a decision you make once per session and then live with; the setting is a decision you can revisit per test. Mobile suites always eventually contain the test that needs the opposite of the default, and discovering that after you've built 400 tests around a capability is an expensive time to find out the layer beneath it was writable all along.

    What changes on a real device

    Two things, and both cut in your favour once you know about them.

    appium:permissions being Simulator-only means real-device iOS permission state comes from the device itself — whatever the last session left behind. On a shared pool that's a source of mystery failures: you don't know which state you inherited. On a dedicated device, that state is yours and it's stable between runs, so "granted at the start of the suite" is something you can establish once and rely on rather than re-derive every session.

    The flip side is that stable state is still state. Make the starting condition explicit at suite setup — grant or revoke deliberately — rather than letting run N inherit run N−1 by accident.

    And when a prompt does slip through in CI, the thing that ends the argument fastest is seeing it: a video of the run and the element tree at the moment of failure tell you in seconds whether the dialog appeared and went unhandled, or never appeared at all. Those two failures produce identical stack traces and completely different fixes.

    Run your Appium suite on a dedicated real device →

    Ready to test on real devices?

    Sign in with Google or GitHub and get real iOS and Android devices in your browser — free to try.

    👋 Hi! Need help? Chat with us!

    Chat with us

    Online

    Before we start

    Share your details so we can follow up with you.