medium priority low complexity testing pending testing specialist Tier 3

Acceptance Criteria

Test 'enqueues events up to capacity': track 50 events and assert mockSink.dispatch was never called
Test 'flushes on 51st event': track 51 events and assert mockSink.dispatch was called exactly once with a batch of 50 events, and the 51st event remains in the buffer
Test 'flushes on background lifecycle signal': track 3 events, simulate AppLifecycleState.paused, and assert dispatch was called with those 3 events
Test 'no-op when sink throws': configure mockSink.dispatch to throw, track an event, trigger flush, and assert no exception propagates to the caller
Test 'serialises all event type variants': one test per AnonymisedExpenseEventType variant asserting the dispatched batch contains a correctly serialised event
Test 'buffer cleared after flush': after a flush, track 1 new event and assert total buffer size is 1 (not 51)
Test 'periodic timer flush': use a fake async clock (FakeAsync) to advance time by 31 seconds and assert dispatch is called if buffer is non-empty
All tests use a MockAnalyticsSink (mockito or manual mock) — zero real network calls
Test suite completes in under 500 ms

Technical Requirements

frameworks
flutter_test
mockito
fake_async
data models
AnonymisedExpenseEvent
AnonymisedExpenseEventType
ExpenseType
AnalyticsSink
performance requirements
Tests complete in < 500 ms total
FakeAsync used for timer-dependent tests — no real sleep() calls

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

The lifecycle signal test requires calling the tracker's didChangeAppLifecycleState(AppLifecycleState.paused) method directly rather than going through the Flutter binding — structure the tracker so this method is accessible for testing. For the FakeAsync timer test, the tracker must accept an optional Clock or TimerFactory in its constructor (or via the Riverpod override) so the fake clock can control the periodic timer. The 'no-op when sink throws' test should verify that trackEvent() and the public interface remain callable after a sink failure — the tracker must not enter a broken state. Use verify(mockSink.dispatch(captureAny)) from mockito to assert both call count and argument content.

Testing Requirements

Pure unit tests with flutter_test. Use mockito's @GenerateMocks([AnalyticsSink]) or a handwritten mock implementing the AnalyticsSink interface. Use the fake_async package to control Timer.periodic without real time delays. Group tests by scenario: 'buffer management', 'flush triggers', 'error resilience', 'serialisation'.

Each group should have 2-3 focused tests. Avoid testing internals — test only the public API (trackEvent, flush, lifecycle signal) and the observable side effect (mockSink.dispatch call count and arguments).

Component
Expense Type Analytics Tracker
infrastructure low
Epic Risks (2)
high impact medium prob scope

The compatibility matrix might be under-specified in source documentation. If a new organisation adds expense types or redefines rules, hardcoded pairwise logic becomes a maintenance liability and can silently allow previously excluded combinations.

Mitigation & Contingency

Mitigation: Model the matrix as a const Map<ExpenseType, Set<ExpenseType>> rather than if-else chains; add a unit test that exhaustively asserts every pair combination so any future matrix change forces explicit test updates.

Contingency: If per-organisation matrix variants are requested before the epic closes, extract matrix loading into expense-type-config with an org-override slot and defer per-org configuration to the repository epic.

medium impact medium prob technical

VoiceOver (iOS) and TalkBack (Android) handle Semantics widget announcements differently in Flutter. Live-region behaviour for disabled state changes is inconsistent across Flutter versions and may require platform-specific workarounds that are not yet documented.

Mitigation & Contingency

Mitigation: Write accessibility integration tests using Flutter's SemanticsController targeting both iOS and Android simulators from the outset; pin to a Flutter version known to handle Semantics.liveRegion correctly.

Contingency: If platform parity is unachievable before release, ship with a known gap documented in the WCAG audit log and schedule a dedicated accessibility sprint; do not block other epics.