A Flutter integration test of checkout hangs in CI when the Android emulator shows a location permission dialog; why can't the test tap it, and how do you handle it?
answer
- finders search Flutter's widget tree
- the dialog is drawn by Android
- no default timeout on the integration binding
- pre-grant or fake the permission
- native UI automation for the rest
basics
~20 sThe permission dialog is native Android UI, outside Flutter's widget tree, so flutter_test finders cannot see or tap it. Pre-grant the permission before the run or inject a fake permission service, and add timeouts so a blocked test fails instead of hanging.
solid answer
~50 s`find` and `tester.tap` operate on Flutter's element tree and dispatch pointer events into Flutter's gesture system. The runtime permission prompt is an Android system window above the Flutter view, so to the test it does not exist: `find.text('Allow')` matches nothing, and checkout waits for a permission result that never arrives. Because `IntegrationTestWidgetsFlutterBinding` sets no default test timeout, the job hangs until CI kills it. I avoid the dialog rather than fight it: grant the permission before the test with the emulator's package manager, or build the test entrypoint with a fake permission or location service that returns granted. Where the dialog itself must be verified, that part belongs to a native UI automation tool that can see system UI. Either way I set `timeout:` on `testWidgets` so a surprise dialog fails fast with a message.
code
dart · 24 lines// integration_test/checkout_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:shop/app.dart';
import 'package:shop/location/location_service.dart';
class GrantedLocationService implements LocationService {
@override
Future<bool> ensurePermission() async => true;
@override
Future<Position> currentPosition() async => const Position(lat: 52.37, lng: 4.90);
}
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('checkout suggests the nearest pickup point', (WidgetTester tester) async {
await tester.pumpWidget(ShopApp(location: GrantedLocationService()));
await tester.pumpAndSettle();
// ... add to cart, open checkout ...
expect(find.textContaining('Pickup:'), findsOneWidget);
}, timeout: const Timeout(Duration(minutes: 3)));
}go deeper
Recall that integration_test finders only see Flutter widgets, so system dialogs such as permission prompts cannot be tapped from the test.
Explain why the test hangs - the flow waits on the dialog and the integration binding has no default timeout - and the options of pre-granting or faking the permission.
Design the entrypoint with seams for permission and location services, add per-test timeouts and bounded waits, and move prompt and denial coverage to native UI automation.
Decide which native interactions deserve automated coverage and in which toolset, weighing the cost of a second test stack against manual checks.
## What the test can and cannot see An integration test drives the app with **`flutter_test`** APIs. Every finder - `find.text`, `find.byKey`, `find.byType` - walks **Flutter's element tree**, and every gesture helper - `tap`, `drag`, `enterText` - synthesises pointer or text-input events and feeds them into **Flutter's own gesture and text systems**. Nothing in that path reaches outside the Flutter view. A runtime permission request on Android is drawn by the **operating system**, in its own window, above the app. The same is true of: - the iOS permission alerts and the notifications prompt, - the system share sheet and document pickers, - the biometric prompt, - another app opened by an intent or URL, - the notification shade and system settings screens. To a `flutter_test` finder these are invisible, and a `tap` at their coordinates is delivered to Flutter's widgets underneath, not to the dialog. ## Why it hangs instead of failing 1. The checkout screen asks for location to suggest a pickup point; a plugin shows the system dialog and waits. 2. The test tries `await tester.pumpAndSettle()` or waits for the next step's finder; the dialog never goes away, so the flow never advances. 3. `IntegrationTestWidgetsFlutterBinding` sets **`defaultTestTimeout` to none**, so nothing inside the test stops it; `flutter drive` has no timeout by default either. 4. The CI job finally hits its own limit and is killed, often with no useful message. ## How to handle it | Approach | How | When it fits | |---|---|---| | Pre-grant the permission | grant it with the emulator's package manager (for example via `adb` before the test run) or install with runtime permissions granted | the test is about checkout, not the prompt | | Fake the dependency | a test entrypoint injects a permission or location service that returns granted and a fixed position | deterministic runs, no device state needed | | Avoid triggering it | seed app state so the feature needing permission is not on this path | the journey can be tested without it | | Native UI automation | drive the dialog with a tool that sees system UI | the prompt, denial and "don't ask again" paths must be verified | Notes on each: - **Pre-granting** is closest to reality: the real plugin runs, sees the permission already granted and returns without showing UI. Reset app data between runs so an earlier grant or denial does not leak into the next one. - **Faking** needs the app to receive its permission or location service through a seam - a constructor parameter, a provider override or a service locator - that the test entrypoint can replace. It removes the device dependency entirely, and it also tests less of the real integration. - **Native automation** belongs to a different toolset (Espresso and UI Automator on Android, XCUITest on iOS, or cross-platform tools built on them). Keep that set of tests small and focused on the permission flows. ## Make failures fast and readable - Pass `timeout: const Timeout(Duration(minutes: 3))` to `testWidgets` (or `--timeout` to `flutter drive`) so a surprise dialog produces a test failure, not a killed job. - Replace open-ended waits with a bounded loop that pumps until a finder matches and fails with a message naming the step. - When a run with `flutter drive` stalls, `--screenshot <dir>` together with `--timeout` captures the screen at the moment of failure - which usually shows the dialog. ## The interview point The honest answer to "can integration_test handle native dialogs?" is **no, not through its finders**. A strong answer then explains the boundary (Flutter's tree versus the OS's windows), shows how to keep the Flutter journey deterministic (pre-grant or fake), and names where the native part is tested instead.
- Why does tapping at the dialog's screen coordinates with tester.tapAt not work?`tapAt` dispatches a synthesised pointer event into Flutter's gesture binding, not into the operating system's input pipeline. The event reaches whatever Flutter widget is at that position under the dialog; the system window never receives it.
- What is the risk of pre-granting permissions for every integration test?The tests never exercise the denied or 'ask again' paths, and a grant from one run can mask a bug in the next if app data is not reset. Keep a few dedicated tests - often native UI automation - for denial and the prompt itself.
saying these in an interview costs you the question
- find.text('Allow') will locate the Android permission button
- tester.tapAt on the dialog's coordinates dismisses the system prompt
- The integration binding times out a stuck test after ten minutes
- Add a long sleep so someone can tap the dialog during the run
- Native dialogs are rendered by Flutter, so any finder can see them