End-to-end integration test: full biometric session resume flow
epic-biometric-session-authentication-ui-and-lifecycle-task-014 — Write an integration test that exercises the complete biometric session resume flow end-to-end: user logs in with credentials, opts into biometrics on the onboarding prompt, backgrounds the app, resumes the app, BiometricPromptOverlay appears and native dialog is triggered, successful auth resumes the session without credential re-entry. Also cover the degradation path where biometrics fail and BiometricUnavailableBanner is shown with OS settings link.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
The biggest complexity here is intercepting the native LocalAuthentication channel reliably without flakiness. Register a mock message handler via TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler() before runApp() is called. Return pre-scripted responses (success/failure/not-enrolled) from the handler. Ensure the app's dependency injection bootstrapper can accept an injected Supabase client and SecureStorage so the test can supply in-memory fakes.
Use find.byKey() with explicit ValueKey constants (defined in the widget source) rather than find.text() for overlay and banner detection, as text is localized and may change. For the 'background and resume' simulation, call binding.handleAppLifecycleStateChanged(AppLifecycleState.paused) then AppLifecycleState.resumed rather than pressing the home button, which is not reliably automatable across platforms.
Testing Requirements
Use the Flutter integration_test package with testWidgets() running on a real simulator/emulator (not just a widget test). Platform channels for LocalAuthentication must be intercepted using TestDefaultBinaryMessengerBinding or a channel mock registered before the app launches. Supabase client should be initialized with a mock HTTP client that returns pre-scripted responses (login success, session valid/expired). Structure the test file with shared setUp() that creates a clean app state and tearDown() that disposes all streams and resets channel mocks.
Each scenario (happy path, degradation, opt-out, expiry) should be a separate testWidgets() block to enable individual re-runs on failure. Distribute tests across TestFlight (iOS) and a standard Android emulator in CI pipeline.
Flutter's AppLifecycleState.resumed event fires in scenarios beyond simple user-initiated app-switching: it also fires when the native biometric dialog itself dismisses and returns control to Flutter, when system alerts (low battery, notifications) temporarily cover the app, and when the app is foregrounded by a deep link. Without careful debouncing and state tracking, SessionResumeManager can trigger multiple overlapping biometric prompts or prompt immediately after a just-completed authentication, creating a confusing loop.
Mitigation & Contingency
Mitigation: Implement a state machine inside SessionResumeManager with explicit states (idle, prompting, authenticated, awaiting-fallback) and guard all prompt triggers with a state check. Add a minimum inter-prompt interval of 3 seconds. Write widget tests that simulate rapid lifecycle event sequences and verify only one prompt is shown.
Contingency: If the state machine approach proves difficult to test or maintain, replace it with a simple boolean isPromptActive flag with a debounce timer, accepting slightly less precise semantics in exchange for simpler reasoning about concurrent lifecycle events.
The BiometricPromptOverlay appears on top of the existing app content when the app resumes. If focus is not correctly transferred to the overlay and returned to the underlying screen after authentication, screen reader users (VoiceOver/TalkBack) will either be unable to interact with the prompt or will lose their navigation position in the app after authentication completes, violating WCAG 2.2 focus management requirements and creating a broken experience for Blindeforbundet users.
Mitigation & Contingency
Mitigation: Use Flutter's FocusScope and FocusTrap utilities to capture focus within the overlay on presentation and restore the previous FocusNode after dismissal. Add a live region announcement (using the accessibility live region announcer component) when the overlay appears. Include dedicated VoiceOver and TalkBack test cases in the acceptance criteria.
Contingency: If FocusTrap behaviour proves unreliable across Flutter versions, implement the overlay as a full Navigator push to a modal route rather than an overlay widget, which gives Flutter's built-in modal semantics and focus management automatic WCAG-compliant behaviour.
The BiometricUnavailableBanner needs to deep-link to biometric enrollment settings on both iOS and Android. iOS uses a single URL scheme (app-settings:) that opens the app's settings page. Android has no universal URL for biometric settings — the correct Intent action (Settings.ACTION_BIOMETRIC_ENROLL) was introduced in API 30, with different fallback actions required for API 23-29. Using the wrong action on older Android devices either crashes or navigates to an unrelated settings screen.
Mitigation & Contingency
Mitigation: Build an Android API version check into LocalAuthIntegration that selects the correct Intent action based on the runtime SDK version. Test against Android API 23, 28, and 30+ in the CI matrix. For iOS, validate that the app-settings: URL scheme is correctly declared.
Contingency: If the Android settings Intent fragmentation cannot be resolved reliably for all target API levels, fall back to navigating to the top-level Settings screen (Settings.ACTION_SETTINGS) with an overlay instruction telling the user to navigate to 'Security > Biometrics' manually, ensuring the user always has a path to resolve the issue even if the deep link is imprecise.