diff --git a/example/lib/pages/home/data/home_api_provider.dart b/example/lib/pages/home/data/home_api_provider.dart index afe4a530..e76bfcdd 100644 --- a/example/lib/pages/home/data/home_api_provider.dart +++ b/example/lib/pages/home/data/home_api_provider.dart @@ -22,14 +22,8 @@ class HomeProvider extends GetConnect implements IHomeProvider { Future>> getCountries() { return get( '/countries', - decoder: (data) { - print(data.runtimeType); - final foo = (data as List).map((item) { - return CountriesItem.fromJson(item); - }); - print("foo: ${foo.runtimeType}"); - return foo.toList(); - }, + decoder: (data) => + (data as List).map((item) => CountriesItem.fromJson(item)).toList(), ); } diff --git a/example/lib/pages/home/presentation/controllers/home_controller.dart b/example/lib/pages/home/presentation/controllers/home_controller.dart index 197f7909..aeac8abb 100644 --- a/example/lib/pages/home/presentation/controllers/home_controller.dart +++ b/example/lib/pages/home/presentation/controllers/home_controller.dart @@ -11,14 +11,7 @@ class HomeController extends StateController> { @override void onInit() { super.onInit(); - change(GetStatus.success([])); - change(GetStatus.loading()); - //Loading, Success, Error handle with 1 line of code - try { - futurize(homeRepository.getCountries); - } catch (e) { - print(e); - } + futurize(homeRepository.getCountries); } Future getCountryByName(String name) async { diff --git a/example/test/main_test.dart b/example/test/main_test.dart index 1e48d5db..ea062bde 100644 --- a/example/test/main_test.dart +++ b/example/test/main_test.dart @@ -3,12 +3,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; +import 'package:get/get_navigation/src/routes/test_kit.dart'; import 'package:get_demo/pages/home/domain/adapters/repository_adapter.dart'; import 'package:get_demo/pages/home/domain/entity/country_model.dart'; +import 'package:get_demo/pages/home/presentation/controllers/details_controller.dart'; import 'package:get_demo/pages/home/presentation/controllers/home_controller.dart'; -// import 'package:get_demo/routes/app_pages.dart'; -// import 'package:get_test/get_test.dart'; +// Mock data const country1 = CountriesItem( country: 'Lalaland', countryCode: 'LA', @@ -19,136 +20,127 @@ const country2 = CountriesItem( countryCode: 'LO', ); +// Mock repository for success class MockRepositorySuccess implements IHomeRepository { - Future> getCountries() { - return Future.value([ - country1, - country2, - ]); - } - - Future getCountry(String path) { - return Future.value(Country( - name: 'Lalaland', - countryCode: 'LA', - numberOfPrizes: 3, - averageAgeOfLaureates: 4, - )); - } + @override + Future> getCountries() async => [country1, country2]; + + @override + Future getCountry(String path) async => Country( + name: 'Lalaland', + countryCode: 'LA', + numberOfPrizes: 3, + averageAgeOfLaureates: 4, + ); } +// Mock repository for failure class MockRepositoryFailure implements IHomeRepository { - Future> getCountries() { - return Future>.error('error'); - } + @override + Future> getCountries() async => + Future.error(FetchException('Failed to load countries')); - Future getCountry(String path) { - return Future.error('error'); - } + @override + Future getCountry(String path) async => + Future.error(FetchException('Failed to load country')); +} + +class FetchException implements Exception { + final String message; + FetchException(this.message); +} + +// Custom bindings +class TestHomeBinding extends Binding { + final IHomeRepository repository; + TestHomeBinding({required this.repository}); + + @override + List dependencies() => [ + Bind.lazyPut(() => repository), + Bind.lazyPut( + () => HomeController(homeRepository: Get.find()), + ), + ]; } -class MockBinding extends Binding { +class TestDetailsBinding extends Binding { + final IHomeRepository repository; + TestDetailsBinding({required this.repository}); + @override - List dependencies() { - return [ - Bind.lazyPut(() => MockRepositorySuccess()), - Bind.lazyPut( - () => HomeController(homeRepository: Get.find()), - ) - ]; - } + List dependencies() => [ + Bind.lazyPut(() => repository), + Bind.lazyPut( + () => DetailsController(homeRepository: Get.find()), + ), + ]; } void main() { WidgetsFlutterBinding.ensureInitialized(); - setUpAll(() => HttpOverrides.global = null); - final binding = MockBinding(); + setUpAll(() { + HttpOverrides.global = null; + GetTestMode.active = true; + }); + + setUp(() => Get.reset()); - test('Test Controller', () async { - /// Controller can't be on memory - expect(() => Get.find(tag: 'success'), - throwsA(const TypeMatcher())); + group('HomeController Tests', () { + test('Success Scenario', () async { + TestHomeBinding(repository: MockRepositorySuccess()).dependencies(); + final controller = Get.find(); - /// binding will put the controller on memory - binding.dependencies(); + expect(controller.initialized, isTrue); - /// recover your controller - HomeController controller = Get.find(); + await Future.delayed(const Duration(milliseconds: 200)); - /// check if onInit was called - expect(controller.initialized, true); + expect(controller.status.isSuccess, isTrue); + expect(controller.state.length, 2); + expect(controller.state, containsAll([country1, country2])); + }); - /// check initial Status - expect(controller.status.isLoading, true); + test('Failure Scenario', () async { + TestHomeBinding(repository: MockRepositoryFailure()).dependencies(); + final controller = Get.find(); - /// await time request - await Future.delayed(const Duration(milliseconds: 100)); + expect(controller.initialized, isTrue); - /// test if status is success - expect(controller.status.isSuccess, true); - expect(controller.state.length, 2); - expect(controller.state.first, country1); - expect(controller.state.last, country2); + await Future.delayed(const Duration(milliseconds: 200)); - /// test if status is error - Get.lazyReplace(() => MockRepositoryFailure()); - controller = Get.find(); - print(controller.getCountryByName('Lalaland')); - // expect(controller.status.isError, true); - // expect(controller.state, null); + expect(controller.status.isError, isTrue); + expect(controller.status.error, isA()); + }); }); - /// Tests with GetTests - /// TEMPORARILY REMOVED from the null-safetym branch as - /// get_test is not yet null safety. - /* getTest( - "test description", - getPages: AppPages.routes, - initialRoute: AppPages.INITIAL, - widgetTest: (tester) async { - expect('/home', Get.currentRoute); - - Get.toNamed('/home/country'); - expect('/home/country', Get.currentRoute); - - Get.toNamed('/home/country/details'); - expect('/home/country/details', Get.currentRoute); - - Get.back(); - - expect('/home/country', Get.currentRoute); - }, - ); - - testGetX( - 'GetX test', - widget: GetX( - init: Controller(), - builder: (controller) { - return Text("ban:${controller.count}"); - }, - ), - test: (e) { - expect(find.text("ban:0"), findsOneWidget); - }, - ); - - testController( - 'Controller test', - (controller) { - print('controllllllll ${controller.count}'); - }, - controller: Controller(), - onInit: (c) { - c.increment(); - print('onInit'); - }, - onReady: (c) { - print('onReady'); - c.increment(); - }, - onClose: (c) { - print('onClose'); - }, - );*/ + group('DetailsController Tests', () { + test('Success Scenario', () async { + TestDetailsBinding(repository: MockRepositorySuccess()).dependencies(); + GetTestMode.setTestArguments(country1); + final controller = Get.find(); + + expect(controller.initialized, isTrue); + + await Future.delayed(const Duration(milliseconds: 200)); + + expect(controller.status.isSuccess, isTrue); + expect(controller.state.name, 'Lalaland'); + expect(controller.state.countryCode, 'LA'); + expect(controller.state.numberOfPrizes, 3); + expect(controller.state.averageAgeOfLaureates, 4); + }); + + test('Failure Scenario', () async { + TestDetailsBinding(repository: MockRepositoryFailure()).dependencies(); + GetTestMode.setTestArguments(country1); + final controller = Get.find(); + + expect(controller.initialized, isTrue); + + await Future.delayed(const Duration(milliseconds: 200)); + + expect(controller.status.isError, isTrue); + expect(controller.status.error, isA()); + }); + }); } diff --git a/lib/get_navigation/src/extension_navigation.dart b/lib/get_navigation/src/extension_navigation.dart index f8beca29..5cb7d981 100644 --- a/lib/get_navigation/src/extension_navigation.dart +++ b/lib/get_navigation/src/extension_navigation.dart @@ -1,6 +1,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; +import 'package:get/get_navigation/src/routes/test_kit.dart'; import '../../get.dart'; import 'dialog/dialog_route.dart'; @@ -1182,10 +1183,6 @@ extension GetNavigationExt on GetInterface { return key; } - /// give current arguments - //dynamic get arguments => routing.args; - dynamic get arguments => rootController.rootDelegate.arguments(); - /// give name from current route String get currentRoute => routing.current; @@ -1326,14 +1323,37 @@ extension GetNavigationExt on GetInterface { Routing get routing => _getxController.routing; - set parameters(Map newParameters) => - rootController.parameters = newParameters; + bool get _shouldUseMock => GetTestMode.active && !GetRoot.treeInitialized; - set testMode(bool isTest) => rootController.testMode = isTest; + /// give current arguments + dynamic get arguments { + return args(); + } - bool get testMode => _getxController.testMode; + T args() { + if (_shouldUseMock) { + return GetTestMode.arguments as T; + } + return rootController.rootDelegate.arguments(); + } - Map get parameters => rootController.rootDelegate.parameters; + // set parameters(Map newParameters) { + // rootController.parameters = newParameters; + // } + + // @Deprecated('Use GetTestMode.active=true instead') + set testMode(bool isTest) => GetTestMode.active = isTest; + + // @Deprecated('Use GetTestMode.active instead') + bool get testMode => GetTestMode.active; + + Map get parameters { + if (_shouldUseMock) { + return GetTestMode.parameters; + } + + return rootController.rootDelegate.parameters; + } /// Casts the stored router delegate to a desired type TDelegate? delegate, TPage>() => diff --git a/lib/get_navigation/src/root/get_root.dart b/lib/get_navigation/src/root/get_root.dart index 1789a00a..b1db2050 100644 --- a/lib/get_navigation/src/root/get_root.dart +++ b/lib/get_navigation/src/root/get_root.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:get/get_navigation/src/routes/test_kit.dart'; import '../../../get.dart'; import '../router_report.dart'; @@ -287,6 +288,8 @@ class GetRoot extends StatefulWidget { @override State createState() => GetRootState(); + static bool get treeInitialized => GetRootState._controller != null; + static GetRootState of(BuildContext context) { // Handles the case where the input context is a navigator element. GetRootState? root; @@ -432,7 +435,7 @@ class GetRootState extends State with WidgetsBindingObserver { set testMode(bool isTest) { config = config.copyWith(testMode: isTest); - // _getxController.testMode = isTest; + GetTestMode.active = isTest; } void onReady() { diff --git a/lib/get_navigation/src/routes/route_middleware.dart b/lib/get_navigation/src/routes/route_middleware.dart index 24294cbb..b3464249 100644 --- a/lib/get_navigation/src/routes/route_middleware.dart +++ b/lib/get_navigation/src/routes/route_middleware.dart @@ -235,7 +235,7 @@ class PageRedirect { settings = route; } final match = context.delegate.matchRoute(settings!.name!); - Get.parameters = match.parameters; + // Get.parameters = match.parameters; // No Match found if (match.route == null) { diff --git a/lib/get_navigation/src/routes/test_kit.dart b/lib/get_navigation/src/routes/test_kit.dart new file mode 100644 index 00000000..3684bc97 --- /dev/null +++ b/lib/get_navigation/src/routes/test_kit.dart @@ -0,0 +1,18 @@ +class GetTestMode { + static bool active = false; + static Object? _arguments; + + static void setTestArguments(Object? arguments) { + _arguments = arguments; + } + + static Object? get arguments => _arguments; + + static Map _parameters = {}; + + static void setTestParameters(Map parameters) { + _parameters = parameters; + } + + static Map get parameters => _parameters; +} diff --git a/lib/get_state_manager/src/rx_flutter/rx_notifier.dart b/lib/get_state_manager/src/rx_flutter/rx_notifier.dart index aa4559e4..6863a69c 100644 --- a/lib/get_state_manager/src/rx_flutter/rx_notifier.dart +++ b/lib/get_state_manager/src/rx_flutter/rx_notifier.dart @@ -71,20 +71,38 @@ mixin StateMixin on ListNotifier { } } + void setSuccess(T data) { + change(GetStatus.success(data)); + } + + void setError(Object error) { + change(GetStatus.error(error)); + } + + void setLoading() { + change(GetStatus.loading()); + } + + void setEmpty() { + change(GetStatus.empty()); + } + void futurize(Future Function() body, {T? initialData, String? errorMessage, bool useEmpty = true}) { final compute = body; _value ??= initialData; + status = GetStatus.loading(); compute().then((newValue) { if ((newValue == null || newValue._isEmpty()) && useEmpty) { - status = GetStatus.loading(); + status = GetStatus.empty(); } else { status = GetStatus.success(newValue); } refresh(); }, onError: (err) { - status = GetStatus.error(errorMessage ?? err.toString()); + status = GetStatus.error( + err is Exception ? err : Exception(errorMessage ?? err.toString())); refresh(); }); } @@ -250,7 +268,7 @@ abstract class GetStatus with Equality { factory GetStatus.loading() => LoadingStatus(); - factory GetStatus.error(String message) => ErrorStatus(message); + factory GetStatus.error(Object message) => ErrorStatus(message); factory GetStatus.empty() => EmptyStatus(); @@ -303,12 +321,22 @@ extension StatusDataExt on GetStatus { bool get isCustom => !isLoading && !isSuccess && !isError && !isEmpty; + dynamic get error { + if (this is ErrorStatus) { + return (this as ErrorStatus).error; + } + return null; + } + String get errorMessage { final isError = this is ErrorStatus; if (isError) { final err = this as ErrorStatus; - if (err.error != null && err.error is String) { - return err.error as String; + if (err.error != null) { + if (err.error is String) { + return err.error as String; + } + return err.error.toString(); } }