diff --git a/packages/fl_query/lib/src/core/online_manager.dart b/packages/fl_query/lib/src/core/online_manager.dart index 7265095..cfc15ea 100644 --- a/packages/fl_query/lib/src/core/online_manager.dart +++ b/packages/fl_query/lib/src/core/online_manager.dart @@ -4,16 +4,16 @@ import 'package:internet_connection_checker/internet_connection_checker.dart'; typedef SetupFn = void Function()? Function( void Function([bool? online]) setOnline); -class _OnlineManager extends Subscribable { +class OnlineManager extends Subscribable { bool? _online; void Function()? _cleanup; SetupFn? _setup; - _OnlineManager() { + OnlineManager([InternetConnectionChecker? connectionChecker]) { + connectionChecker ??= InternetConnectionChecker(); _setup = (listener) { - var subscription = InternetConnectionChecker() - .onStatusChange - .listen((status) => listener()); + var subscription = + connectionChecker!.onStatusChange.listen((status) => listener()); return () { subscription.cancel(); }; @@ -35,7 +35,7 @@ class _OnlineManager extends Subscribable { } } - setEventListener(SetupFn setup) { + void setEventListener(SetupFn setup) { _setup = setup; _cleanup?.call(); _cleanup = setup(([bool? online]) { @@ -69,4 +69,4 @@ class _OnlineManager extends Subscribable { } } -_OnlineManager onlineManager = _OnlineManager(); +OnlineManager onlineManager = OnlineManager(); diff --git a/packages/fl_query/lib/src/core/subscribable.dart b/packages/fl_query/lib/src/core/subscribable.dart index 23e01c4..f8d357e 100644 --- a/packages/fl_query/lib/src/core/subscribable.dart +++ b/packages/fl_query/lib/src/core/subscribable.dart @@ -5,7 +5,7 @@ abstract class Subscribable { List listeners; Subscribable() : listeners = []; - void Function() subscribe(TListener? listener) { + void Function() subscribe([TListener? listener]) { var callback = listener ?? (() => null); listeners.add(callback as TListener); diff --git a/packages/fl_query/pubspec.yaml b/packages/fl_query/pubspec.yaml index 5672a98..842a822 100644 --- a/packages/fl_query/pubspec.yaml +++ b/packages/fl_query/pubspec.yaml @@ -25,6 +25,7 @@ dev_dependencies: coverage: ^1.0.3 http_parser: ^4.0.0 lints: ^1.0.1 + build_runner: ^2.1.7 # The following section is specific to Flutter. # flutter: diff --git a/packages/fl_query/test/src/core/notify_manager_test.dart b/packages/fl_query/test/src/core/notify_manager_test.dart index acf35f4..c2e22c5 100644 --- a/packages/fl_query/test/src/core/notify_manager_test.dart +++ b/packages/fl_query/test/src/core/notify_manager_test.dart @@ -22,7 +22,7 @@ void main() { final NotifyManager notifyManager = NotifyManager(); int called = 0; notifyManager.schedule(() => called++); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(Duration(milliseconds: 1)); expect(called, equals(1)); }, ); @@ -35,7 +35,7 @@ void main() { int level2 = 0; int level3 = 0; callback() async { - await Future.delayed(Duration(seconds: 20)); + await Future.delayed(Duration(milliseconds: 20)); level3++; } @@ -46,7 +46,7 @@ void main() { }); level1++; }); - await Future.delayed(Duration(seconds: 30)); + await Future.delayed(Duration(milliseconds: 30)); expect(level1, equals(1)); expect(level2, equals(1)); expect(level3, equals(1)); diff --git a/packages/fl_query/test/src/core/online_manager_test.dart b/packages/fl_query/test/src/core/online_manager_test.dart new file mode 100644 index 0000000..ac8d021 --- /dev/null +++ b/packages/fl_query/test/src/core/online_manager_test.dart @@ -0,0 +1,118 @@ +import 'dart:async'; + +import 'package:fl_query/src/core/online_manager.dart'; +import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; +import './online_manager_test.mocks.dart'; + +@GenerateMocks([InternetConnectionChecker]) +void main() { + group('OnlineManager', () { + late OnlineManager onlineManagerTest; + late StreamController statusController; + late MockInternetConnectionChecker connectionChecker; + setUp(() { + statusController = StreamController.broadcast(); + statusController.add(InternetConnectionStatus.connected); + connectionChecker = MockInternetConnectionChecker(); + when(connectionChecker.hasConnection) + .thenAnswer((_) => Future.value(true)); + when(connectionChecker.hasListeners) + .thenReturn(statusController.hasListener); + when(connectionChecker.onStatusChange) + .thenAnswer((_) => statusController.stream); + onlineManagerTest = OnlineManager(connectionChecker); + }); + + tearDown(() { + statusController.close(); + }); + + test( + 'isOnline Should return true When InternetConnectionChecker.hasConnection is true', + () async { + bool online = await onlineManagerTest.isOnline(); + expect(online, isTrue); + }, + ); + + test( + "setEventListener Should use _online property When setOnline sets _online = false", + () async { + int count = 0; + + setup(void Function(bool?) setOnline) { + Timer(Duration(milliseconds: 20), () { + count++; + setOnline(false); + }); + return () {}; + } + + onlineManagerTest.setEventListener(setup); + await Future.delayed(Duration(milliseconds: 30)); + expect(count, equals(1)); + onlineManagerTest.isOnline().then((online) { + expect(online, isFalse); + }); + }, + ); + + test( + 'setEventListener Should call previous remove handler When replacing an event listener', + () { + int cb1calls = 0; + int cb2calls = 0; + onlineManagerTest.setEventListener((_) => () => cb1calls++); + onlineManagerTest.setEventListener((_) => () => cb2calls++); + expect(cb1calls, equals(1)); + expect(cb2calls, equals(0)); + }, + ); + test( + 'Should replace default window listener When a new event listener is set', + () { + // Should set the default event listener with window event listeners + final unsubscribe = onlineManagerTest.subscribe(); + verify(connectionChecker.onStatusChange.listen).called(1); + // Should replace the window default event listener by a new one + // and it should call window.removeEventListener twice + onlineManagerTest.setEventListener((online) { + return () => null; + }); + expect(connectionChecker.hasListeners, isFalse); + unsubscribe(); + }, + ); + + test( + 'Should cancel StreamSubscription When last listener unsubscribes', + () { + final unsubscribe1 = onlineManager.subscribe(() => null); + final unsubscribe2 = onlineManager.subscribe(() => null); + + verify(connectionChecker.onStatusChange.listen).called(1); + unsubscribe1(); + expect(connectionChecker.hasListeners, isTrue); + unsubscribe2(); + expect(connectionChecker.hasListeners, isFalse); + }, + ); + + test('should keep setup function even if last listener unsubscribes', () { + int count = 0; + onlineManager.setEventListener((_) => () => count++); + + final unsubscribe1 = onlineManagerTest.subscribe(() => null); + expect(count, equals(1)); + unsubscribe1(); + + final unsubscribe2 = onlineManager.subscribe(() => null); + expect(count, equals(2)); + unsubscribe2(); + }); + }); +} diff --git a/packages/fl_query/test/src/core/online_manager_test.mocks.dart b/packages/fl_query/test/src/core/online_manager_test.mocks.dart new file mode 100644 index 0000000..fc3a6c1 --- /dev/null +++ b/packages/fl_query/test/src/core/online_manager_test.mocks.dart @@ -0,0 +1,82 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in fl_query/test/src/core/online_manager_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:internet_connection_checker/internet_connection_checker.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeDuration_0 extends _i1.Fake implements Duration {} + +class _FakeAddressCheckResult_1 extends _i1.Fake + implements _i2.AddressCheckResult {} + +/// A class which mocks [InternetConnectionChecker]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockInternetConnectionChecker extends _i1.Mock + implements _i2.InternetConnectionChecker { + MockInternetConnectionChecker() { + _i1.throwOnMissingStub(this); + } + + @override + List<_i2.AddressCheckOptions> get addresses => + (super.noSuchMethod(Invocation.getter(#addresses), + returnValue: <_i2.AddressCheckOptions>[]) + as List<_i2.AddressCheckOptions>); + @override + set addresses(List<_i2.AddressCheckOptions>? _addresses) => + super.noSuchMethod(Invocation.setter(#addresses, _addresses), + returnValueForMissingStub: null); + @override + Duration get checkInterval => + (super.noSuchMethod(Invocation.getter(#checkInterval), + returnValue: _FakeDuration_0()) as Duration); + @override + set checkInterval(Duration? _checkInterval) => + super.noSuchMethod(Invocation.setter(#checkInterval, _checkInterval), + returnValueForMissingStub: null); + @override + _i3.Future get hasConnection => + (super.noSuchMethod(Invocation.getter(#hasConnection), + returnValue: Future.value(false)) as _i3.Future); + @override + _i3.Future<_i2.InternetConnectionStatus> get connectionStatus => + (super.noSuchMethod(Invocation.getter(#connectionStatus), + returnValue: Future<_i2.InternetConnectionStatus>.value( + _i2.InternetConnectionStatus.connected)) + as _i3.Future<_i2.InternetConnectionStatus>); + @override + _i3.Stream<_i2.InternetConnectionStatus> get onStatusChange => + (super.noSuchMethod(Invocation.getter(#onStatusChange), + returnValue: Stream<_i2.InternetConnectionStatus>.empty()) + as _i3.Stream<_i2.InternetConnectionStatus>); + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) + as bool); + @override + bool get isActivelyChecking => + (super.noSuchMethod(Invocation.getter(#isActivelyChecking), + returnValue: false) as bool); + @override + _i3.Future<_i2.AddressCheckResult> isHostReachable( + _i2.AddressCheckOptions? options) => + (super.noSuchMethod(Invocation.method(#isHostReachable, [options]), + returnValue: Future<_i2.AddressCheckResult>.value( + _FakeAddressCheckResult_1())) + as _i3.Future<_i2.AddressCheckResult>); +}