import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:relationship_saver/core/network/reachability/network_reachability.dart'; import 'package:relationship_saver/core/network/reachability/network_reachability_provider.dart'; import 'package:relationship_saver/features/sync/sync_background_runner.dart'; import 'package:relationship_saver/features/sync/sync_coordinator.dart'; void main() { testWidgets( 'triggers sync when reachability transitions from offline to online', (WidgetTester tester) async { final _TestReachability reachability = _TestReachability(initial: false); addTearDown(reachability.dispose); int syncCalls = 0; await tester.pumpWidget( ProviderScope( overrides: [ networkReachabilityProvider.overrideWithValue(reachability), syncCoordinatorProvider.overrideWith( (Ref ref) => _TestSyncCoordinator( ref, onSyncNow: () async { syncCalls += 1; return _okResult(); }, ), ), ], child: const Directionality( textDirection: TextDirection.ltr, child: SyncBackgroundRunner(child: SizedBox()), ), ), ); await tester.pumpAndSettle(const Duration(milliseconds: 60)); // Startup trigger always runs once when background sync is enabled. expect(syncCalls, 1); reachability.emit(false); await tester.pumpAndSettle(const Duration(milliseconds: 40)); expect(syncCalls, 1); reachability.emit(true); await tester.pumpAndSettle(const Duration(milliseconds: 60)); expect(syncCalls, 2); }, ); testWidgets('does not trigger extra sync while connectivity stays online', ( WidgetTester tester, ) async { final _TestReachability reachability = _TestReachability(initial: true); addTearDown(reachability.dispose); int syncCalls = 0; await tester.pumpWidget( ProviderScope( overrides: [ networkReachabilityProvider.overrideWithValue(reachability), syncCoordinatorProvider.overrideWith( (Ref ref) => _TestSyncCoordinator( ref, onSyncNow: () async { syncCalls += 1; return _okResult(); }, ), ), ], child: const Directionality( textDirection: TextDirection.ltr, child: SyncBackgroundRunner(child: SizedBox()), ), ), ); await tester.pumpAndSettle(const Duration(milliseconds: 60)); expect(syncCalls, 1); reachability.emit(true); await tester.pumpAndSettle(const Duration(milliseconds: 60)); expect(syncCalls, 1); }); } class _TestSyncCoordinator extends SyncCoordinator { _TestSyncCoordinator(super.ref, {required this.onSyncNow}); final Future Function() onSyncNow; @override Future syncNow() => onSyncNow(); } class _TestReachability implements NetworkReachability { _TestReachability({required bool initial}) : _current = initial; final StreamController _controller = StreamController.broadcast(); bool _current; @override Future isReachable({ Duration timeout = const Duration(seconds: 2), }) async { return _current; } @override Stream watch({Duration timeout = const Duration(seconds: 2)}) { return _controller.stream; } void emit(bool reachable) { _current = reachable; _controller.add(reachable); } Future dispose() async { await _controller.close(); } } SyncRunResult _okResult() { return const SyncRunResult( success: true, operation: 'sync', message: 'ok', pendingAfter: 0, ); }