high priority medium complexity testing pending testing specialist Tier 2

Acceptance Criteria

BiometricPromptOverlay renders organization logo and localized prompt text in all supported locales (nb, en)
Pumping a successful auth event causes the overlay to dismiss and the underlying route is restored within one frame
Pumping a failed auth event renders a retry button and an error message; a second failure still shows retry (not crash)
Tapping the fallback link calls the navigator with the credential login route and overlay is removed from the widget tree
BiometricUnavailableBanner renders the hardware-absent message when BiometricStatus.hardwareUnavailable is provided
BiometricUnavailableBanner renders the not-enrolled message when BiometricStatus.notEnrolled is provided
OS settings deep-link button fires url_launcher with the correct platform settings URI on both hardware-absent and not-enrolled states
All interactive elements pass WCAG 2.2 AA contrast ratio checks via flutter_accessibility_test (minimum 4.5:1 for body text, 3:1 for large text)
All semantics labels on BiometricPromptOverlay and BiometricUnavailableBanner are non-empty and descriptive for screen reader compatibility
No golden-file regressions: snapshots pass for both light and dark theme variants
All tests run in under 10 seconds total with no flakes across 10 consecutive runs

Technical Requirements

frameworks
flutter_test
flutter_accessibility_test
mocktail or mockito
apis
LocalAuthentication (mocked)
url_launcher (mocked)
data models
BiometricStatus enum
SessionState
AuthResult
performance requirements
Full widget test suite completes in under 10 seconds
No memory leaks: all controllers and streams disposed in tearDown
security requirements
Tests must not persist real biometric credentials to disk
Mocked LocalAuthentication must never invoke native platform channel in CI
ui components
BiometricPromptOverlay
BiometricUnavailableBanner
AppButton (fallback/retry)
Design token color assertions (accent colors from styles.css token set)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Inject all platform dependencies (LocalAuthentication, url_launcher) through the widget constructor or a Riverpod provider so tests can substitute fakes without method channel mocking, which is fragile in CI. Use tester.runAsync() only when awaiting real async gaps; prefer synchronous state-driven tests by pushing states directly into the BLoC/notifier. For accessibility checks, wrap the widget under test in a MaterialApp with the project's design token theme so contrast checks reflect real production colors, not Flutter defaults. Avoid using find.byType(GestureDetector) — prefer find.bySemanticsLabel() to keep tests aligned with screen reader behavior.

Store goldens in test/goldens/ with explicit locale and theme suffixes (e.g., biometric_prompt_overlay_nb_dark.png).

Testing Requirements

Widget tests only (no integration test runner required). Use flutter_test WidgetTester with pumpWidget and pump(Duration). Mock LocalAuthentication and url_launcher with mocktail fakes injected via dependency injection or provider overrides (Riverpod overrides preferred). Test both BLoC/Riverpod state transitions by emitting states directly into the widget under test.

Include at least one golden test per widget variant (hardware-absent, not-enrolled, prompt visible, prompt dismissed) using matchesGoldenFile. Run flutter_accessibility_test checkAccessibility() on every pumped widget state. Aim for 100% branch coverage on both overlay and banner widgets.

Component
Biometric Prompt Overlay
ui low
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.