high priority medium complexity testing pending testing specialist Tier 6

Acceptance Criteria

Test file exists at test/widgets/expense_calculation_preview_test.dart
Test 'zero-state' verifies placeholder text is visible when BLoC state has null calculationResult
Test 'zero-state' verifies no ReimbursementLineItem widgets exist in the tree when calculationResult is null
Test 'line items' verifies exactly N ReimbursementLineItem widgets render when CalculationResult contains N entries
Test 'total row' verifies the displayed total matches CalculationResult.totalAmount formatted as Norwegian NOK
Test '300ms SLA' dispatches a state change and calls tester.pump(Duration(milliseconds: 300)) — asserts updated total is visible by that point
Test 'live-region' verifies SemanticsService.announce was called with the correct Norwegian announcement string after a state change
Test 'no initial announcement' verifies SemanticsService.announce is NOT called during initial widget build
Test 'deduplication' verifies SemanticsService.announce is NOT called when the same total is emitted twice
All tests are hermetic — no Supabase or real BLoC calls

Technical Requirements

frameworks
Flutter
flutter_test
BLoC
apis
SemanticsService.announce (mocked/verified via spy)
data models
CalculationResult (totalAmount double, List<LineItemData>)
LineItemData (name String, amount double)
ExpenseSelectionState
ExpenseSelectionBLoC (mocked)
performance requirements
300ms SLA test must use fake async (tester.pump) not real timers to keep test deterministic and fast
ui components
ExpenseCalculationPreview under test
MockExpenseSelectionBLoC
SemanticsService spy/mock

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

To test SemanticsService.announce, the cleanest approach is to wrap the call behind a thin injectable function (typedef or abstract class) that can be swapped in tests. If the codebase already has a live-region-announcer pattern, use the same injection mechanism for consistency. For the 300ms SLA test, set up the BLoC mock to emit state A, call tester.pumpWidget, then use bloc_test's whenListen to emit state B, call tester.pump(Duration(milliseconds: 300)), and assert. Use a StreamController if manual stream control is needed for precise timing.

The deduplication test requires emitting the same state twice via the mock stream and asserting the announce spy was called exactly once. Structure groups as: 'zero-state rendering', 'populated state rendering', 'stream reactivity / 300ms SLA', 'accessibility announcements'.

Testing Requirements

Full widget test suite using flutter_test and bloc_test. For the 300ms SLA test, use tester.pump(const Duration(milliseconds: 300)) after emitting a new BLoC state — do not use real async delays. For the live-region announcement test, override SemanticsService by injecting a mock or use a test double pattern: capture calls to SemanticsService.announce via a wrapper function injected as a dependency. If the app uses a live-region-announcer helper class, mock that class instead.

Use find.byType(ReimbursementLineItem) to count rendered line items. Use find.text() for total amount assertions with exact NOK formatted string. All golden references from task-011 should be kept separate — do not add new goldens here.

Component
Expense Calculation Preview
ui low
Epic Risks (2)
medium impact medium prob technical

If the expense calculation preview subscribes to the full BLoC state stream, every unrelated state property change (e.g. a loading flag toggle) triggers a widget rebuild. With complex card animations for the disabled-state transition, this could cause frame drops on low-end Android devices used by some peer mentors.

Mitigation & Contingency

Mitigation: Use select() on the Riverpod provider to subscribe only to the specific state slice each widget needs; write a performance test asserting rebuild count on a rapid sequence of toggle events.

Contingency: If jank is detected in device testing, replace animated disabled-state transitions with instant opacity changes and defer animation polish to a follow-up sprint.

medium impact low prob integration

The disabled card state requires a specific contrast-safe colour combination that communicates unavailability without relying solely on colour (WCAG 1.4.1). If the current design token palette does not include a disabled-state token with sufficient contrast for text on the disabled background, the widget will either fail WCAG AA or require a last-minute design token addition that could break other components.

Mitigation & Contingency

Mitigation: Audit the existing design token manifest for disabled-state tokens at the start of the epic; if missing, raise with the design lead and add a contrast-validated token before widget implementation begins.

Contingency: If no design review is available, use the established --color-text-disabled and --color-surface-disabled tokens with an added strikethrough or lock icon to satisfy WCAG 1.4.1 non-colour requirement, and document the deviation for design review.