Widget tests for thumbnail and indicator components
epic-receipt-capture-and-attachment-ui-accessibility-task-012 — Write flutter_test widget tests covering: thumbnail renders with progress overlay at varying progress values, remove and replace callbacks fire correctly, indicator displays all three visual states (optional-unattached, required-unattached, attached), threshold stream state change triggers live region semantic announcement, and org-configurable threshold drives correct indicator state. Target 90%+ branch coverage on both components.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Inject the threshold stream and org config as constructor parameters on both widgets to make them easily testable without BLoC setup. Use a FakeAsync or manual StreamController to control timing. For the live region test, ensure the Semantics widget wrapping the indicator has liveRegion: true set, then verify via tester.getSemantics().hasFlag(SemanticsFlag.isLiveRegion). For progress overlay tests, pump with specific progress values and use find.byType() and widget property assertions rather than pixel-level checks.
Group tests per component using group() blocks for readability. Avoid testing internal implementation details — test observable widget output only (rendered children, semantics, callback calls).
Testing Requirements
All tests are flutter_test widget tests using WidgetTester. Use MockCallback (via a simple void Function() variable capture) to verify callback invocation. Use StreamController to drive threshold stream state changes and await tester.pump() after each emission. Use tester.getSemantics() and SemanticsFlag.isLiveRegion to verify accessibility announcements.
Use lcov/flutter test --coverage to measure branch coverage and fail CI if below 90%. No integration tests or real Supabase calls are needed for this task.
Flutter's accessibility live region support (SemanticsProperties.liveRegion) has known inconsistencies between iOS VoiceOver and Android TalkBack, and between Flutter versions. Threshold-crossing announcements may fail to fire or double-fire, breaking the accessibility contract for Blindeforbundet users.
Mitigation & Contingency
Mitigation: Test live region announcements on physical devices with VoiceOver and TalkBack enabled from the first iteration. Use the AccessibilityLiveRegionAnnouncer component pattern already established in the project. Verify announcement timing relative to Bloc state emissions to avoid double-fires.
Contingency: If Flutter live regions prove unreliable, implement a platform-channel fallback that calls UIAccessibility.post(notification:) on iOS and AccessibilityManager.sendAccessibilityEvent() on Android directly, bypassing Flutter's abstraction.
The org-configurable threshold must be available at form-render time. If the threshold configuration is fetched asynchronously and not cached, the indicator may briefly show the wrong state (e.g., 'optional' before the threshold loads), confusing users and potentially allowing invalid submissions.
Mitigation & Contingency
Mitigation: Ensure the receipt threshold validator loads and caches the org configuration at app startup or organization selection time, not lazily on form open. Use a loading state in the indicator widget rather than defaulting to 'optional' while configuration is pending.
Contingency: If startup caching is not feasible, treat an unknown threshold as 'receipt required' (fail safe) and surface a clear loading indicator until the configuration resolves, preventing invalid submissions while the config loads.