medium priority medium complexity testing pending testing specialist Tier 9

Acceptance Criteria

Widget test exists and passes for the loading skeleton state: asserts that skeleton shimmer widgets are present and slide content widgets are absent
Widget test exists and passes for the loaded first slide state: asserts that the first slide widget renders with correct data values from the injected mock state, and the progress indicator shows position 1
Widget test exists and passes for loaded slide navigation: programmatically triggers next-slide action and asserts that slide index advances, content changes, and progress indicator updates
Widget test exists and passes for offline banner with stale timestamp: injects an offline state variant and asserts the offline banner widget is visible and displays the expected stale-data timestamp string
Widget test exists and passes for sharing loading overlay: injects a sharing-in-progress state and asserts the loading overlay widget is present and interactive elements are disabled
Widget test exists and passes for error state with retry: injects an error state and asserts an error message widget is visible and a retry button widget is present; taps retry and asserts the correct BLoC event is dispatched
Widget test exists and passes for empty state: injects a state where no summary data is available and asserts the empty state widget is shown with appropriate messaging
All 7 states are tested with a mocked WrappedSummaryBloc stream — no real Supabase or network calls are made in any widget test
Tests use bloc_test's MockBloc or equivalent pattern to inject states; no real BLoC instances are used
All widget tests complete in under 5 seconds total (no async delays, no real timers)
Tests follow the existing widget test file naming and structure conventions in the repository

Technical Requirements

frameworks
flutter_test (testWidgets, WidgetTester)
bloc_test (MockBloc, whenListen, stubState for injecting BLoC states)
Riverpod (ProviderScope with overrides for any providers consumed by WrappedSummaryScreen outside BLoC)
mocktail or mockito (for mocking ancillary dependencies such as share_plus or navigation)
apis
No external APIs — all tests are purely local widget tests with mocked BLoC streams
data models
annual_summary (fields used to construct mock WrappedSummaryLoaded state: activity_count, total_hours, year, period_type)
activity (activity counts referenced in mock summary state for slide content assertions)
performance requirements
Widget test suite for this screen completes in under 5 seconds on a developer machine
No fake timers or real async delays — use tester.pump() and tester.pumpAndSettle() with explicit durations where needed
security requirements
No real credentials, real user data, or real Supabase connections used in widget tests
Mock summary data must not include real PII — use placeholder names and fictional statistics
ui components
WrappedSummaryScreen (widget under test)
LoadingSkeletonWidget (asserted for presence in loading state)
SlideWidget (asserted for content in loaded states)
ProgressIndicatorWidget (asserted for correct position value)
OfflineBannerWidget (asserted for visibility and timestamp in offline state)
SharingOverlayWidget (asserted for visibility and interaction blocking in sharing state)
ErrorStateWidget (asserted for message and retry button in error state)
EmptyStateWidget (asserted for visibility in empty state)

Execution Context

Execution Tier
Tier 9

Tier 9 - 22 tasks

Can start after Tier 8 completes

Implementation Notes

Create a test helper file (e.g. test/helpers/wrapped_summary_test_helpers.dart) with factory functions that build the mock BLoC and inject standard test states — this reduces boilerplate across the 7 tests. For the offline state, ensure the WrappedSummaryBloc state model includes an isOffline bool and lastFetchedAt DateTime so the widget can render the banner and timestamp — if these fields don't exist yet, coordinate with the task that defines the state model. For the sharing loading overlay state, the BLoC state may be a variant like WrappedSummarySharing extends WrappedSummaryState — assert that InteractiveViewer or GestureDetector on the slide area has ignoringSemantics: true or equivalent blocking during this state.

Use tester.pumpWidget() followed by tester.pump() (not pumpAndSettle()) in most cases to avoid infinite animation loops from any shimmer or slide-transition animations — pass explicit durations where needed. Follow existing test file conventions (e.g. snake_case file names, _test.dart suffix, test group labelling).

Testing Requirements

This task IS the testing requirement. Use flutter_test's testWidgets for all tests. Inject mock BLoC states using bloc_test's MockBloc with whenListen to emit a stream of states. Wrap the widget under test in a MaterialApp + BlocProvider(create: (_) => mockBloc) + ProviderScope for Riverpod providers.

Use find.byType() and find.byKey() to assert widget presence/absence. For the retry test, use tester.tap(find.byType(RetryButton)) and then verify(mockBloc.add(argThat(isA()))) using mocktail. Group tests by state name for readability. Each test should be fully independent — no shared state between tests.

Component
Wrapped Summary Screen
ui high
Epic Risks (3)
medium impact medium prob technical

If the device transitions between online and offline states while the user is mid-session in the wrapped screen, the BLoC may emit conflicting state transitions (loaded → error → offline) that cause visual flickering or an inconsistent UI state such as showing the offline banner over an already-loaded summary.

Mitigation & Contingency

Mitigation: Implement a connectivity stream listener in the BLoC that only triggers a state re-evaluation when transitioning from online to offline, not on every connectivity event. Once a summary is in the Loaded state, the BLoC should not transition to error/offline unless the user explicitly requests a refresh. Store the last-loaded data in BLoC state so it survives connectivity changes.

Contingency: If state flickering is observed in testing, add a minimum 3-second debounce on connectivity state changes before the BLoC reacts, and display a non-blocking top banner rather than replacing the entire screen state.

high impact medium prob integration

The push notification deep-link to the wrapped-summary-screen must work correctly whether the app is in the foreground, background, or terminated state. Handling all three app launch states on both iOS and Android is a common source of edge-case bugs, particularly when authentication state must be restored before the deep link can be resolved.

Mitigation & Contingency

Mitigation: Implement deep-link handling through the existing notification-deep-link-handler component which already manages app-state-aware routing. Define the wrapped-summary route in the navigation config early in the epic so the router is ready before notification dispatch is wired. Test all three app states (foreground, background, terminated) explicitly in the QA checklist.

Contingency: If terminated-state deep-linking fails on specific platforms, fall back to launching the app to the home screen with an in-app notification banner prompting the user to open their summary, rather than direct deep-link navigation.

high impact low prob technical

The wrapped-summary-screen manages a large number of AnimationController instances (one or more per slide) via the wrapped-animation-controller. If disposal is not triggered correctly when the user exits mid-flow (e.g., via system back gesture or deep-link away), memory leaks will accumulate across session navigation.

Mitigation & Contingency

Mitigation: Implement screen disposal via Flutter's dispose() lifecycle method calling a single wrapped-animation-controller.disposeAll() method that iterates the named controller registry. Write a test that navigates to the screen, starts animations, then navigates away and verifies no active AnimationController listeners remain using Flutter's test binding.

Contingency: If disposal bugs are detected in production via memory profiling, patch by converting all AnimationControllers to use AutomaticKeepAliveClientMixin false and wrap each slide in a widget that disposes its own controller when removed from the widget tree.