high priority medium complexity testing pending testing specialist Tier 4

Acceptance Criteria

Thumbnail widget test renders correctly at progress values 0.0, 0.25, 0.5, 0.75, and 1.0 with visible progress overlay at intermediate values and no overlay at 1.0
Thumbnail remove callback fires exactly once when remove action is triggered, verified via mock callback invocation count
Thumbnail replace callback fires exactly once when replace action is triggered, verified via mock callback invocation count
Indicator widget test renders optional-unattached state with correct visual style (e.g., dashed border, no fill icon)
Indicator widget test renders required-unattached state with correct visual style (e.g., red/error color, warning icon)
Indicator widget test renders attached state with correct visual style (e.g., solid border, checkmark icon)
Threshold stream emitting a state change causes a Semantics live region announcement to be fired, verified via tester.getSemantics()
Org-configurable threshold value of 'required' sets indicator to required-unattached when no receipt is attached
Org-configurable threshold value of 'optional' sets indicator to optional-unattached when no receipt is attached
Branch coverage for both thumbnail and indicator components reaches 90%+ as reported by flutter test --coverage
All tests pass in CI without flakiness across 3 consecutive runs
No golden file tests are introduced — widget state is verified via Semantics tree and widget finder assertions only

Technical Requirements

frameworks
Flutter
flutter_test
BLoC
data models
OrgConfig (threshold field)
ReceiptAttachmentState
performance requirements
All widget tests must complete within 30 seconds total
No real file I/O or network calls in any test — all dependencies mocked
security requirements
No real user data or receipt images used in tests — use placeholder assets only
ui components
ReceiptThumbnailPreview widget
ReceiptAttachmentIndicator widget
Progress overlay widget
Semantics live region wrapper

Execution Context

Execution Tier
Tier 4

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.

Epic Risks (2)
medium impact medium prob technical

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.

medium impact low prob integration

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.