high priority high complexity testing pending testing specialist Tier 4

Acceptance Criteria

Happy path: credential login completes, biometric onboarding prompt appears, user opts in, app is backgrounded and resumed, BiometricPromptOverlay is displayed, native biometric dialog is triggered (mock returns success), home screen is shown without credential re-entry
Degradation path: same flow but native biometric mock returns failure; BiometricUnavailableBanner is shown with correct message and OS settings link visible
Biometric opt-out path: user dismisses onboarding prompt, resumes app, credential login screen is shown (not biometric overlay)
Session expiry path: session token is pre-expired in Supabase mock before resume; credential login screen is shown regardless of biometric status
No data is persisted between test runs: Supabase mock and SecureStorage are fully reset in tearDown
Test runs on both iOS simulator (TestFlight-compatible build) and Android emulator without platform-specific skips
Integration test completes in under 60 seconds on a standard CI runner
OS settings link tap is intercepted by a url_launcher mock and the correct URI is asserted (no real device settings opened)

Technical Requirements

frameworks
flutter_test (integration_test package)
mocktail (for platform channel fakes)
supabase_flutter (test mode or mock client)
apis
LocalAuthentication platform channel (stubbed)
Supabase Auth API (mocked or local emulator)
url_launcher (mocked)
SecureStorage (in-memory test implementation)
data models
SessionState
AuthCredentials
BiometricStatus
UserProfile
performance requirements
Full integration test suite completes in under 60 seconds
No real network calls: Supabase must use a mock/emulator client
App startup within test must not exceed 5 seconds
security requirements
SecureStorage replaced with an in-memory implementation for all integration tests
No real BankID or Supabase credentials used in CI
Test session tokens must be clearly synthetic (e.g., prefix 'test_') and never logged to stdout
ui components
BiometricPromptOverlay
BiometricUnavailableBanner
BiometricOnboardingPrompt
CredentialLoginScreen
HomeScreen

Execution Context

Execution Tier
Tier 4

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.

Component
Session Resume Manager
service medium
Epic Risks (3)
medium impact high prob technical

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.

high impact medium prob technical

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.

low impact high prob technical

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.