high priority medium complexity testing pending testing specialist Tier 5

Acceptance Criteria

Unit tests for ReceiptAttachmentService cover: successful upload flow emits uploading then uploaded states; failed upload emits failed state with error message; cancelUpload() terminates upload and emits idle state; retryUpload() re-initiates upload from failed state
Unit tests for ReceiptImageCompressor verify: output file size is below the configured maximum bytes; output dimensions do not exceed configured maximum pixels; EXIF metadata is absent from output; compression is a no-op for images already below the size threshold
Unit tests for ReceiptThresholdValidator verify: returns true (mandatory) when amount >= 100; returns false when amount < 100; returns false when amount is null or zero; boundary condition at exactly 100 kr returns true
Widget tests for ReceiptCameraSheet verify: both action buttons are rendered with correct labels; tapping camera button calls ReceiptImagePickerIntegration.pickFromCamera(); tapping gallery button calls ReceiptImagePickerIntegration.pickFromGallery(); dismissal without selection returns null
Widget tests for ReceiptAttachmentIndicator verify: idle state renders attach button; uploading state renders progress bar and percentage; uploaded state renders thumbnail and remove button; failed state renders retry button and error message; mandatory indicator visible when isMandatory is true and state is idle
All tests use mocktail for dependencies — no real file I/O, no real network calls, no real Supabase SDK calls
Test coverage for all tested files is at minimum 80% line coverage
All tests pass in CI with zero flaky failures across 3 consecutive runs
Tests are organised in clearly named test groups (group() blocks) matching the class under test

Technical Requirements

frameworks
Flutter
flutter_test
mocktail
apis
ReceiptAttachmentService (internal)
ReceiptImageCompressor (internal)
ReceiptThresholdValidator (internal)
ReceiptCameraSheet (internal)
ReceiptAttachmentIndicator (internal)
performance requirements
Full test suite for this epic must complete within 60 seconds in CI
security requirements
No real credentials, API keys, or Supabase URLs in test code — all external dependencies mocked
No real image files containing PII used as test fixtures — use programmatically generated solid-color test images

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Structure test files mirroring source structure: receipt_attachment_service_test.dart, receipt_image_compressor_test.dart, receipt_threshold_validator_test.dart, receipt_camera_sheet_test.dart, receipt_attachment_indicator_test.dart. Create a test_helpers/ directory with: a factory for generating test XFile instances (solid-color PNG bytes, no EXIF), and a MockReceiptAttachmentService class using mocktail's Mock. For ReceiptImageCompressor tests, generate a synthetic oversized image in memory using dart:ui or the image package — do not rely on asset files. For ReceiptAttachmentIndicator widget tests, provide a StreamController and manually add events, then call tester.pump() to trigger rebuilds.

Use group() nesting: outer group for class name, inner group for method/scenario name. Avoid test interdependency — each test must be fully independent with its own setup. Add a README comment at the top of each test file explaining what is being tested and what is mocked.

Testing Requirements

Unit tests (pure Dart, no widget pump needed) for ReceiptAttachmentService, ReceiptImageCompressor, and ReceiptThresholdValidator. Widget tests (WidgetTester) for ReceiptCameraSheet and ReceiptAttachmentIndicator. Use StreamController in tests to emit controlled states from ReceiptAttachmentService mock. Use fake_async for timer-dependent behaviour if any timeouts exist in ReceiptAttachmentService.

Verify state transitions by collecting emitted events from the service stream using expectLater with emitsInOrder. For widget tests, use pump() and pumpAndSettle() carefully — do not use pumpAndSettle() when streams are active as it may time out.

Component
Receipt Attachment Service
service high
Epic Risks (3)
high impact medium prob technical

Non-blocking upload creates a race condition: if the claim record is submitted and saved before the upload completes, the storage path may never be written to the claim_receipts table, leaving the claim with a missing receipt that was nonetheless required.

Mitigation & Contingency

Mitigation: Design the attachment service to queue a completion callback that writes the storage path to the claim record upon upload completion, even after the claim form has submitted. Use a local task queue with persistence to survive app backgrounding. Test the race condition explicitly with simulated slow uploads.

Contingency: If the async path association proves unreliable, fall back to blocking upload before claim submission with a clear progress indicator, accepting the UX trade-off in exchange for data integrity.

high impact medium prob scope

The offline capture requirement (cache locally, sync when connected) significantly increases state management complexity. If the offline queue is not durable, receipts captured without connectivity may be lost when the app is killed, causing claim submission failures users are not aware of.

Mitigation & Contingency

Mitigation: Persist the offline upload queue to local storage (e.g., Hive or SQLite) on every state transition. Implement background sync using WorkManager (Android) and BGTaskScheduler (iOS). Scope the initial delivery to online-only flow if offline sync cannot be adequately tested before release.

Contingency: Ship without offline support in the first release, displaying a clear 'Upload requires connection' message. Add offline sync as a follow-on task once the core online flow is validated in production.

medium impact low prob integration

The inline bottom sheet presentation within a multi-step wizard can conflict with existing modal navigation and back-button handling, particularly if the expense wizard itself uses nested navigation or custom route management.

Mitigation & Contingency

Mitigation: Review the expense wizard navigation architecture before implementation. Use showModalBottomSheet with barrier dismissal disabled to prevent accidental dismissal. Coordinate with the expense wizard team on modal stacking behavior and ensure the camera sheet does not interfere with wizard step transitions.

Contingency: If modal stacking causes navigation issues, present the camera sheet as a full-screen dialog using PageRouteBuilder with a transparent barrier, preserving wizard state via the existing Bloc while still appearing inline.