How to Test Permission Dialogs on Flutter (Complete Guide)

How to Test Permission Dialogs on Flutter (Complete Guide)

February 19, 2026 · 14 min read · How-To Guides

How to Test Permission Dialogs on Flutter (Complete Guide)

Testing permission dialogs in Flutter apps is critical because a mishandled request can lead to crashed screens, denied‑by‑default user flows, privacy complaints, or even App Store rejection. This guide walks you through why permission handling matters, builds a comprehensive test matrix, shows manual and automated techniques, introduces Flutter‑specific tooling, and explains how autonomous, persona‑driven exploration surfaces bugs that scripted tests miss. Every section contains concrete steps, code examples, and tables you can copy into your own project.

Why Permission Dialog Testing Matters in Flutter

Impact on user trust and compliance

When a Flutter app asks for camera, location, or microphone access, the operating system presents a native dialog. If the app does not handle the user’s response correctly—whether they tap Allow, Deny, or Deny‑don’t‑ask‑again—the UI can freeze, navigation can break, or sensitive data can be accessed without proper consent. In regulated markets (GDPR, CCPA, HIPAA) missing a proper rationale or failing to respect a persistent deny can result in legal penalties and negative store reviews. A single uncaught permission bug often shows up only after a release because the dialog depends on the device’s OS version, language settings, and the user’s prior interaction history.

Common failure modes in production

  1. Unhandled denial – The app proceeds as if permission was granted, leading to a crash when trying to access the protected resource.
  2. Missing rationale – On Android 6+ and iOS, the system may show a rationale dialog only if the app provides a custom explanation; omitting it causes the system to deny automatically after a few attempts.
  3. Incorrect state restoration – After a denial, navigating away and returning to the permission screen may not re‑prompt, leaving the user stuck.
  4. Accessibility gaps – TalkBack or VoiceOver may not announce the dialog’s options, making it unusable for users who rely on screen readers.
  5. Security‑phishing vectors – A malicious overlay can mimic the system permission dialog; if the app does not verify the source of the callback, it could grant privileges to a fake prompt.

Understanding these failure modes shapes the test matrix that follows.

Test Matrix for Permission Dialogs

The table below enumerates the core scenarios you should cover for each permission type (camera, location, microphone, contacts, storage, etc.). Adjust rows for permissions that are relevant to your app.

PermissionTest IDScenarioPreconditionsActionExpected ResultPost‑condition
CameraCAM‑01Happy path – first‑time requestNo prior permission stateTrigger camera pickerSystem dialog shows Allow/Deny; tapping Allow grants permissionApp proceeds to camera preview
CameraCAM‑02Deny – first‑time requestNo prior permission stateTrigger camera picker; tap DenyPermission denied; app receives false from permission_handlerApp shows rationale or disables camera feature
CameraCAM‑03Deny‑don’t‑ask‑againPreviously denied onceTrigger camera picker; tap Deny & check “Don’t ask again”System does not show dialog on subsequent triggers; permission_handler returns false permanentlyApp must provide a settings‑shortcut to re‑enable
LocationLOC‑01While‑in‑use requestLocation not grantedTrigger location fetchDialog shows Allow while using app / Allow only this time / DenyApp receives appropriate status
LocationLOC‑02Background request (Android 10+)While‑in‑use grantedRequest background locationSeparate dialog for background access appearsApp only gets background permission if user grants
MicrophoneMIC‑01Interrupted by callMicrophone grantedStart audio recording; receive phone callRecording pauses; system may re‑show dialog after call endsApp resumes or stops gracefully
StorageSTO‑01Scoped storage (Android 11+)No storage permissionAttempt to save file to external directorySystem shows scoped storage dialog; allowed access to app‑specific folder onlyFile written to app‑specific directory
ContactsCON‑01Permission revoked via settingsPermission grantedGo to system settings → Apps → Your app → Permissions → Contacts → DenyNext attempt to read contacts returns empty list; no dialog shownApp handles empty data state

Notes on the matrix

A second table compares the effort and coverage of manual versus automated techniques for each scenario.

TechniqueSetup TimeExecution SpeedCoverage (Happy/Error/Edge)Maintenance OverheadBest For
Manual device testingLow (just a device)Slow (human)High (can observe subtle UX)Low (no code)Exploratory, accessibility checks
Unit test with mocksMedium (mock platform channel)Fast (sub‑second)Medium (logic only)Low (mock updates)Verifying permission‑handler wrappers
Integration test (flutter_driver)High (setup driver)Medium (device/emulator)High (UI + logic)Medium (test flakiness)CI pipelines, regression
Autonomous persona exploration (SUSA)Very low (CLI install)Medium (depends on depth)Very high (covers unexpected paths)Low (no test code)Finding regressions, edge‑case bugs

Manual Testing Approach Step‑by‑Step

Setting up a device or emulator

  1. Choose a representative OS version – For Android, test API 23 (runtime permissions introduced) and the latest API; for iOS, test iOS 13+ where permission prompts changed.
  2. Clear app data – Run adb shell pm clear com.example.yourapp or delete the app from the simulator to guarantee a fresh permission state.
  3. Enable accessibility services – Turn on TalkBack (Android) or VoiceOver (iOS) to verify that the dialog is announced correctly.
  4. Install a logging tool – Use adb logcat | grep Permission or Xcode’s console to capture the callback from permission_handler.

Triggering the dialog

Observing behavior

Recording results

Create a simple spreadsheet with columns matching the test matrix (Test ID, Scenario, Result, Comments, Evidence). Attach screenshots or short video clips for any failed case. This artifact becomes the baseline for future regression checks.

Automated Testing with Flutter Test and Integration Test

Unit testing permission logic with mocks

The permission_handler plugin communicates with the native side via platform channels. In unit tests you can replace the channel with a mock using the mockito package.


import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:permission_handler/permission_handler.dart';

// Mock the MethodChannel used by permission_handler
class MockMethodChannel extends Mock implements MethodChannel {}

void main() {
  late MockMethodChannel mockChannel;
  setUp(() {
    mockChannel = MockMethodChannel();
    // Inject the mock into the plugin (requires exposing a setter or using dependency injection)
    PermissionHandlerMock.bindMock(mockChannel);
  });

  test('requestCamera returns true when user allows', () async {
    when(mockChannel.invokeMethod<bool>('Permission.request', any))
        .thenAnswer((_) async => true);

    final status = await Permission.camera.request();
    expect(status, isTrue);
  });

  test('requestCamera returns false when user denies', () async {
    when(mockChannel.invokeMethod<bool>('Permission.request', any))
        .thenAnswer((_) async => false);

    final status = await Permission.camera.request();
    expect(status, isFalse);
  });
}

*Key points*:

Integration test using flutter_driver

Integration tests exercise the full Flutter tree on a device or emulator. The integration_test package provides a driver‑based API.

  1. Add dependencies in dev_dependencies:

integration_test:
  sdk: flutter
flutter_test:
  sdk: flutter
  1. Create integration_test/permission_test.dart:

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:your_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Permission flow', () {
    testCameraRequest() async {
      app.main();
      await tester.pumpAndSettle();

      // Tap button that triggers camera request
      await tester.tap(find.byKey(const Key('cameraButton')));
      await tester.pump(); // let the native dialog appear

      // On Android we cannot interact with the native dialog from Flutter,
      // but we can verify that the callback fires.
      // Simulate the platform channel response:
      const MethodChannel channel = MethodChannel('flutter.baseflow.com/permissions/permission_handler');
      await channel.invokeMethod<bool>('Permission.request', <String, dynamic>{'permission': 'camera'}).then((value) {
        // value is what the plugin would return; we assert UI reflects it
        expect(value, equals(true)); // assume we mocked Allow
      });

      // Verify UI updates (e.g., camera preview appears)
      await tester.pumpAndSettle();
      expect(find.byType(CameraPreview), findsOneWidget);
    }
  });
}

*Note*: Direct interaction with the system permission dialog is not possible from Flutter tests because it runs outside the Flutter engine. The pattern above shows how to mock the platform channel response at the test level, letting you assert UI changes based on the granted/denied outcome.

Using golden tests for UI

Golden tests ensure that permission‑related UI (e.g., a rationale dialog you build) renders correctly across screen sizes and font scales.


testWidgets('Permission rationale renders correctly', (tester) async {
  await tester.pumpWidget(MaterialApp(
    home: Scaffold(
      body: PermissionRationaleWidget(permission: Permission.camera),
    ),
  ));
  await tester.pump(const Duration(seconds: 1));
  expect(await tester.goldenMatcher, matchesGoldenFile('permission_rationale_camera.png'));
});

Run flutter test --update-goldens to regenerate baselines when you intentionally change the UI.

Tooling Specific to Flutter

permission_handler package testing

The permission_handler plugin is the de‑facto way to request runtime permissions. Its API returns a PermissionStatus enum (granted, denied, deniedForever, limited, provisional). When writing tests, remember:

Always check the plugin’s changelog for breaking changes; the test matrix should be revisited whenever you upgrade.

mockito for mocking platform channels

As shown in the unit‑test snippet, mockito lets you stub the MethodChannel used by permission_handler. For more complex interactions (e.g., handling a stream of permission status changes), you can mock StreamChannel:


when(mockChannel.invokeMethod<bool>('Permission.request', any))
    .thenAnswer((_) async => false);
when(mockChannel.getMethodCallHandler())
    .thenAnswer((_) => _MockStreamHandler());

integration_test with flutter_test

The integration_test package runs on a real device or emulator, giving you confidence that the native dialog appears. Use the flutter drive command to execute:


flutter drive \
  --target=integration_test/permission_test.dart \
  --dry-run   # (optional) verify script correctness

For CI, consider using Firebase Test Lab or GitHub Actions with Android emulator and iOS simulators.

Using Firebase Test Lab

Firebase Test Lab lets you run your integration tests on a matrix of devices and OS versions without maintaining a local farm.

  1. Build an APK for Android and an IPA for iOS.
  2. Upload to Test Lab via the console or gcloud CLI:
  3. 
       gcloud firebase test android run \
         --type instrumentation \
         --app app-debug.apk \
         --test integration_test.apk \
         --device model=Pixel3,version=33,locale=en,orientation=portrait
    
  4. Review the test logs for any permission‑related failures; the platform logs show whether the dialog was displayed and how the app reacted.

Autonomous, Persona‑Driven Exploration with SUSA

How SUSA discovers permission dialogs

SUSA (SUSATest) explores an app without pre‑written scripts by simulating real‑world user behaviors. It starts from the launcher icon, taps, scrolls, types, and reacts to system dialogs—including permission prompts—based on the active persona’s profile. When a permission dialog appears, SUSA records:

Because SUSA does not rely on hardcoded locators, it can reach permission triggers that are hidden behind dynamic UI states (e.g., a permission request that only appears after a user completes a multi‑step onboarding flow).

Persona profiles that trigger edge cases

SUSA ships with several built‑in personas, each with distinct tendencies:

PersonaBehavior traits relevant to permissions
CuriousTries every button, often taps Allow immediately to see what happens.
ImpatientRapidly taps through dialogs, sometimes double‑tapping Deny.
NoviceReads the rationale carefully, may tap Learn More or Settings before deciding.
AdversarialActively denies permissions, then attempts to force‑grant via settings or overlay attacks.
ElderlySlower interactions, may miss timed dialogs, relies heavily on screen reader announcements.
AccessibilityUses TalkBack/VoiceOver exclusively; verifies that focus lands on each action and that labels are spoken.
Power userFrequently toggles permissions via settings, tests revocation/re‑grant cycles.

When SUSA runs with the Adversarial persona, it attempts to overlay a fake permission dialog using SYSTEM_ALERT_WINDOW (if the app mistakenly grants that permission) to see if the app distinguishes the real system prompt from a spoof. The Accessibility persona checks that the dialog’s actions are correctly announced and that the focus order respects logical grouping.

Example of a bug found only by autonomous testing

In a recent Flutter e‑commerce app, SUSA’s Novice persona repeatedly triggered the location permission request while browsing product details. The app’s code only requested location when the user pressed “Find Nearby Stores”. However, a hidden analytics module called LocationService.getCurrentPosition() on every page view, causing a permission prompt to appear unexpectedly. The Novice persona, after reading the rationale, tapped “Allow while using the app” but then immediately pressed the back button, leaving the app in a state where the location callback was never invoked, resulting in a silent failure of the store‑finder feature. Manual test scripts that followed the happy‑path navigation never saw this background request, so the bug escaped detection until SUSA flagged it via a mismatch between expected UI (store list) and actual UI (empty list) after the persona’s exploration path.

Accessibility and Security Considerations

WCAG checks for permission dialogs

Even though the permission dialog is native, your app still has responsibilities:

  1. Contrast and text size – Ensure any custom rationale you show before calling the system dialog meets WCAG AA contrast (≥4.5:1) and supports dynamic type.
  2. Labeling – If you display a custom explanation, provide accessible labels (Semantics.label) so screen readers can read it.
  3. Focus management – After the system dialog is dismissed, return focus to the element that triggered the request (e.g., the button) to avoid disorienting users.
  4. Error messages – If a permission is denied and a feature cannot be used, convey the issue via an accessible toast or snackbar with an actionable “Open settings” link.

You can automate some of these checks using the flutter_launcher_icons package’s accessibility scanner or by running adb shell am start -a android.intent.action.VIEW -d "https://developer.android.com/guide/topics/ui/accessibility" on a device and using the built‑in accessibility test service.

Preventing phishing‑style fake dialogs

A malicious app could try to draw an overlay that mimics the system permission dialog. Defenses include:

Ensuring proper rationale strings

Both Android and iOS require a short explanation in the manifest or Info.plist. Test that these strings appear in the system dialog:

Checklist and Takeaways

Quick reference checklist

Item
1Verify each permission type used by the app has a corresponding row in the test matrix.
2Test happy path (grant) and both denial paths (temporary and permanent).
3Confirm that custom rationale strings appear in the system dialog when required.
4Check accessibility: screen reader announces each option and focus returns correctly after dismissal.
5Simulate revocation via system settings while the app is foreground; ensure graceful handling.
6Run unit tests with mocked platform channels for all permission‑handler wrappers.
7Execute integration tests on at least one Android API ≥2 and one iOS simulator ≥ 8Run autonomous exploration (e.g., SUSA) with the Accessibility and Adversarial personas to surface hidden prompts.
9Inspect logs for any PlatformException or unexpected permission status after each interaction.
10Document any deviation from expected behavior and add a regression test.

Final recommendations

By following the matrix, applying both manual and automated techniques, and validating with autonomous, persona‑driven testing, you’ll ship Flutter apps that respect user privacy, stay compliant with regulations, and avoid the embarrassing permission‑related bugs that slip through in release builds. Happy testing!

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free